Glance
Publish agent-created pages and review them through shareable links and comments.
Glance is a self-hosted Claude Artifacts alternative built on Cloudflare (D1, Durable Objects, KV, R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.
Source & license
Upstream license: MIT
License TL;DR
You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.
Explain MIT in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The documented Glance deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.
Hosting requirements
- Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
- Keep aggregate D1 use below 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; a request can touch many rows.
- Keep KV below 100,000 reads/day, 1,000 writes, deletes and list operations/day each, and 1 GB; cache refreshes and backups consume writes.
- Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
- Use the configured SQLite Durable Object classes within 100,000 requests/day, 13,000 GB-s duration/day, 5 million SQL rows read/day, 100,000 written/day and 5 GB storage; active sockets consume duration.
- Only ordinary Free-eligible Workers AI models are covered, within 10,000 neurons/day; optional external providers and paid-only models are excluded.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: ff602b1. Hosting eligibility reflects the deployment documentation and listed assumptions.
- claude-artifacts ↗
Works with Claude Code, Cursor, Codex, Cline, Aider, or anything else that can run a shell command. Runs on Cloudflare's free tier.
- workers ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "glance", "main": "src/index.ts", "account_id": "YOUR_ACCOUNT_ID", // EDIT: replace with your own (see README) "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "workers_dev": true, "observability": { "enabled": true }, // Non-secret config. Secrets (GOOGLE_CLIENT_ID/SECRET, SESSION_SECRET, and the optional // DATA_TOKEN_
- d1 ↗
": "ASSETS", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*"] }, "d1_databases": [ { "binding": "GLANCE_DB", "database_name": "glance-db", "database_id": "YOUR_D1_DATABASE_ID", // EDIT: replace with your own (see README) "migrations_dir": "drizzle" } ], // One hibernating Durable Object per site, fanning out glance.db change events to subscribed // pages (src/realtime/site-room.ts, re-exported from src/index.ts so class_name
- kv ↗
w_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice-comment transcription (Whisper). Optional at runtime (AI?: Ai) — // a deploy/dev without it degrades to a transcript placeholder, never an error. Workers AI has NO // local simulator, so `remote: true` proxies inference to the real
- r2 ↗
: "SITE_ROOM", "class_name": "SiteRoom" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice-comment transcription (Whisper). Optional at runtime (AI?: Ai) — // a deploy/dev without it degrades to a transcript placeholder, never an error. W
- durable-objects ↗
es). // new_sqlite_classes, NOT new_classes: KV-backed Durable Objects are not on the free plan. "durable_objects": { "bindings": [{ "name": "SITE_ROOM", "class_name": "SiteRoom" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice
- workers-ai ↗
SSION_SECRET, and the optional // DATA_TOKEN_SECRET for the glance.db shared backend) via `wrangler secret put`. // setup.sh wires the YOUR-SUBDOMAIN sentinel from the live workers.dev URL on deploy. "vars": { "APP_URL": "https://glance.YOUR-SUBDOMAIN.workers.dev", // EDIT: replace with your own (see README) "CONTENT_URL": "https://glance-content.YOUR-SUBDOMAIN.workers.dev", // EDIT: replace with your own (see README) "ALLOWED_HD": "yourcompany.com", // EDIT: replace with your ow
- free-tier-eligible ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "glance", "main": "src/index.ts", "account_id": "YOUR_ACCOUNT_ID", // EDIT: replace with your own (see README) "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "workers_dev": true, "observability": { "enabled": true }, // Non-secret config. Secrets (GOOGLE_CLIENT_ID/SECRET, SESSION_SECRET, and the optional // DATA_TOKEN_
- free-tier-eligible ↗
": "ASSETS", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*"] }, "d1_databases": [ { "binding": "GLANCE_DB", "database_name": "glance-db", "database_id": "YOUR_D1_DATABASE_ID", // EDIT: replace with your own (see README) "migrations_dir": "drizzle" } ], // One hibernating Durable Object per site, fanning out glance.db change events to subscribed // pages (src/realtime/site-room.ts, re-exported from src/index.ts so class_name
- free-tier-eligible ↗
w_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice-comment transcription (Whisper). Optional at runtime (AI?: Ai) — // a deploy/dev without it degrades to a transcript placeholder, never an error. Workers AI has NO // local simulator, so `remote: true` proxies inference to the real
- free-tier-eligible ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- free-tier-eligible ↗
oudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/observability/metrics-analytics/#query-via-the-graphql-api), or the [Cloudflare dashboard ↗︎](https://dash.cloudflare.com/?to=/:account/workers/d1/). Select your D1 database, then vie
- free-tier-eligible ↗
cing/). | | Free plan<sup>1</sup> | Paid plan | | --- | --- | --- | | Keys read | 100,000 / day | 10 million/month, + $0.50/million | | Keys written | 1,000 / day | 1 million/month, + $5.00/million | | Keys deleted | 1,000 / day | 1 million/month, + $5.00/million | | List requests | 1,000 / day | 1 million/month, + $5.00/million | | Stored data | 1 GB | 1 GB, + $0.50/ GB-month | <sup>1</sup> The Workers Free plan includes limited Workers KV usage. All limits reset daily at 00:00 UTC. If you exceed any one of these limits, further operations of that type will fail with an error. Note Workers KV pricing for read, write and delete operations is on a per-key basis. Bulk read operations are billed by the amount
- free-tier-eligible ↗
infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp
- free-tier-eligible ↗
jects are available both on Workers Free and Workers Paid plans. - **Workers Free plan**: Only Durable Objects with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) are available. - **Workers Paid plan**: Durable Objects with the SQLite storage backend are available. The [key-value storage backend](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/#storage-backends) is only available to accounts that already have a key-value-backed namespace. If you wish to downgrade from a Workers Paid plan to a Workers Free plan, you must first ensure that you have deleted all Durable Object namespaces with the key-value storage backend. On Workers Free plan: - If you exceed any one of the free tier limits, further operations of that type will fail with an error. - Daily free limits reset at 00:00 UTC. ## Compute billing Durable Objects are billed for compute duration (wall-clock time) while the Durable Object is actively running or is idle in memory but unable to [hibernate](https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/). Durable Objects that are idle and eligible for hibernation are not billed for duration, even before the runtime has hibernated them. Requests to a D
- free-tier-eligible ↗
000 Neurons**. Our free allocation allows anyone to use a total of **10,000 Neurons per day at no charge**. To use more than 10,000 Neurons per day, you need to sign up for the [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/#workers). On Workers Paid, you will be charged at $0.011 / 1,000 Neurons for any usage above the free allocation of 10,000 Neurons per day. You can monitor your Neuron usage in the [Cloudflare Workers AI dashboard ↗︎](https://dash.cloudflare.com/?to=/:account/ai/workers-ai). All limits reset daily at 00:00 UTC. If you exceed any one of the above limits, further operations will fail with an error. | | Free <br> allocation | Pricing | | --- | --- | --- | |
- free-tier-eligible ↗
billed accordingly. | | Free plan | Paid plan | | --- | --- | --- | | Requests | 100,000 / day | 1 million / month, + $0.15/million<br> Includes HTTP requests, RPC sessions<sup>1</sup>, WebSocket messages<sup>2</sup>, and alarm invocations | | Duration<sup>3</sup> | 13,000 GB-s / day | 400,000 GB-s / month, + $12.50/million GB-s<sup>4,5</sup> | <details> <summary> Footnotes </summary> <sup>1</sup> Each <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/">RPC session</a> is billed as one request to your Durable Object. Every <a href="https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/">RPC method call</a> on a <a href="https://developers.cloudflare.com/durable-objects/">Durable Objects stub</a> is its own RPC session and therefore a single billed request. RPC method calls can return objects (stubs) extending <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#lifetimes-memory-and-resource-management"><code>RpcTarget</code></a> and invo
- free-tier-eligible ↗
/). | | Workers Free plan | Workers Paid plan | | --- | --- | --- | | Rows reads <sup>1,2</sup> | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written <sup>1,2,3,4</sup> | 100,000 / day | First 50 million / month included + $1.00 / million rows | | SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month | <details> <summary> Footnotes </summary> <sup>1</sup> Rows read and rows written included limits and rates match <a href="https://developers.cloudflare.com/d1/platform/pricing/">D1 pricing</a>, Cloudflare's serverless SQL database. <sup>2</sup> Key-value methods like <code>get()</code>, <code>put()</code>, <code>delete()</code>, or <code>list(
- MIT ↗
MIT License Copyright (c) 2026 Plivo Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRIN
- architecture ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "glance", "main": "src/index.ts", "account_id": "YOUR_ACCOUNT_ID", // EDIT: replace with your own (see README) "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "workers_dev": true, "observability": { "enabled": true }, // Non-secret config. Secrets (GOOGLE_CLIENT_ID/SECRET, SESSION_SECRET, and the optional // DATA_TOKEN_
- architecture ↗
": "ASSETS", "not_found_handling": "single-page-application", "run_worker_first": ["/api/*"] }, "d1_databases": [ { "binding": "GLANCE_DB", "database_name": "glance-db", "database_id": "YOUR_D1_DATABASE_ID", // EDIT: replace with your own (see README) "migrations_dir": "drizzle" } ], // One hibernating Durable Object per site, fanning out glance.db change events to subscribed // pages (src/realtime/site-room.ts, re-exported from src/index.ts so class_name
- architecture ↗
w_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice-comment transcription (Whisper). Optional at runtime (AI?: Ai) — // a deploy/dev without it degrades to a transcript placeholder, never an error. Workers AI has NO // local simulator, so `remote: true` proxies inference to the real
- architecture ↗
: "SITE_ROOM", "class_name": "SiteRoom" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice-comment transcription (Whisper). Optional at runtime (AI?: Ai) — // a deploy/dev without it degrades to a transcript placeholder, never an error. W
- architecture ↗
es). // new_sqlite_classes, NOT new_classes: KV-backed Durable Objects are not on the free plan. "durable_objects": { "bindings": [{ "name": "SITE_ROOM", "class_name": "SiteRoom" }] }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SiteRoom"] }], "r2_buckets": [{ "binding": "GLANCE_FILES", "bucket_name": "glance-files" }], "kv_namespaces": [{ "binding": "GLANCE_SESSIONS", "id": "YOUR_KV_NAMESPACE_ID" }], // EDIT: replace with your own (see README) // Workers AI: server-side voice
- architecture ↗
SSION_SECRET, and the optional // DATA_TOKEN_SECRET for the glance.db shared backend) via `wrangler secret put`. // setup.sh wires the YOUR-SUBDOMAIN sentinel from the live workers.dev URL on deploy. "vars": { "APP_URL": "https://glance.YOUR-SUBDOMAIN.workers.dev", // EDIT: replace with your own (see README) "CONTENT_URL": "https://glance-content.YOUR-SUBDOMAIN.workers.dev", // EDIT: replace with your own (see README) "ALLOWED_HD": "yourcompany.com", // EDIT: replace with your ow
- architecture ↗
served from a separate domain, so they can't read your login session. That's why Glance runs two Workers. To report a vulnerability, see [SECURITY.md](SECURITY.md). ## Development Built with Cloudflare Workers + Hono, React Router v7, D1, R2, and KV. The CLI is written in Go. ``` packages/api Worker: API + file serving packages/web React app packages/cli glance CLI ``` For local setup and checks, see [CONTRIBUTING.md](CONTRIBUTING.md). ## License [MIT](LICENSE)
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Publishing agent-created HTML/pages and reviewing them with share links and comments; Claude models, generation and subscription features are excluded.
See supporting source ↗How it works
The shape of Glance on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit ff602b197427. Files were read as data; upstream applications and CI jobs were not executed.
Partial source coverage: 123 files outside collection bounds; 0 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.
Deployment configuration · 2 files
Cloudflare Workers · compatibility 2026-06-01
glance · default
Entrypoint: src/index.ts
Static assets: ../web/dist · single-page-application · Worker first: ["/api/*"]
Cron triggers (UTC): 0 * * * * · 0 3 * * *
GLANCE_DB→ D1GLANCE_SESSIONS→ KVGLANCE_FILES→ R2SITE_ROOM→ Durable Objects · class SiteRoomAI→ Workers AIASSETS→ Static assets
Cloudflare Workers · compatibility 2026-06-01
glance-content · default
Entrypoint: src/content.ts
GLANCE_DB→ D1GLANCE_FILES→ R2
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L159 · fetch handler exported
- L160 · scheduled handler exported · references GLANCE_DB, GLANCE_SESSIONS · calls sessionDb, purgeRetention, cachedStats, p.then
- L40 · app.use("*")
- L65 · app.get("/api/install")
- L77 · app.get("/api/glance.js")
- L83 · app.route("/api/themes")
- L88 · app.route("/api/_data")
- L90 · app.use("/api/*")
- L91 · app.use("/api/*")
- L92 · app.use("/api/*")
- L94 · app.get("/api/health")
- L98 · app.get("/api/config")
- L107 · app.route("/api/auth")
- L108 · app.route("/api/spaces")
- L109 · app.route("/api/sites")
- L116 · app.route("/api/sites")
- L119 · app.route("/api/sites")
- L121 · app.route("/api/sites")
- L123 · app.route("/api/sites")
- L124 · app.route("/api/comments")
- L125 · app.route("/api/upload")
- L127 · app.route("/api/data-token")
- L128 · app.route("/api/users")
- L131 · app.route("/api/avatars")
- L132 · app.route("/api/notifications")
- L134 · app.route("/api/api-keys")
- L135 · app.route("/api/whats-new")
- L136 · app.route("/api/admin")
- L140 · app.route("/api/slack")
Environment references: c.env.CONTENT_URL · c.env.BOOTSTRAP_TOKEN · env.GLANCE_DB · env.GLANCE_SESSIONS
- L69 · app.get("/")
- L74 · app.get("/_glance/annotate.js")
- L77 · app.get("/_glance/annotate.css")
- L80 · app.get("/_glance/db.js")
- L86 · app.get("/_glance/theme/:file{[a-z0-9-]+\\.css}")
- L96 · app.get("/_glance/theme/fonts/:file{[a-z0-9-]+\\.woff2}")
- L115 · app.get("/_glance/og/:space/:site{[a-z0-9-]+\\.png}")
- L137 · app.get("/_t/:token/:space/:site/*")
- L146 · app.get("/:space/:site/*")
- L42 · getDb calls (conditional paths may differ): c.get, sessionDb
- L49 · getCache calls (conditional paths may differ): c.get
- L58 · readStoredObject calls (conditional paths may differ): readFullObject, getCache, fireAndForget
- L65 · notFound calls (conditional paths may differ): c.text
- L149 · serve calls (conditional paths may differ): getDb, normalizePath, limit, where, innerJoin, from, db.select, eq, and, reqPath.endsWith, fetchAccessFacts, notFound, c.text, checkAccess, isSharedFromFacts, reqPath.slice, filter, all.map, p.startsWith, directoryListing
- L333 · serveStoredObject calls (conditional paths may differ): themeHref.replace, etag.endsWith, etag.slice, c.req.header, c.env.GLANCE_FILES.head, notFound, withTheme, headers.set, view, headers.delete, c.env.GLANCE_FILES.get, decideRange, serveFullDirect, readStoredObject, transformServedHtml
- L461 · trackView calls (conditional paths may differ): fireAndForget, recordEvent
- L467 · restOf calls (conditional paths may differ): pathname.endsWith, map, slice, filter, pathname.split, decodeURIComponent, segs.join
- L485 · isHtmlFile calls (conditional paths may differ): test
- L494 · injectAnnotate calls (conditional paths may differ): replace, JSON.stringify, html.includes, html.replace
- L518 · injectDb calls (conditional paths may differ): replace, JSON.stringify, html.includes, html.replace, test, exec, html.slice
- L566 · transformServedHtml calls (conditional paths may differ): on, el.getAttribute, isExternalHref, el.setAttribute, rewriter.on, el.append, rewriter.onDocument, end.append, rewriter.transform
- L597 · normalizePath calls (conditional paths may differ): rest.endsWith, join, filter, rest.split
- L621 · markdownCsp calls (conditional paths may differ): join
- L641 · directoryListing calls (conditional paths may differ): sort, filter, paths.map, p.slice, join, rels.map, map, full.split, escapeHtml, c.html
- L664 · renderMarkdownDoc calls (conditional paths may differ): escapeHtml
Environment references: c.env.GLANCE_DB · c.env.GLANCE_FILES · c.env.CONTENT_TOKEN_SECRET · c.env.OG_RENDER · c.env.APP_URL
- L12 · sessionDb calls (conditional paths may differ): drizzle, binding.withSession
Environment references: c.env.GLANCE_DB
- L23 · getUserById calls (conditional paths may differ): limit, where, from, db.select, eq
- L39 · getUserByEmail calls (conditional paths may differ): limit, where, from, db.select, eq, email.toLowerCase
- L52 · superadminExists calls (conditional paths may differ): limit, where, from, db.select, eq
- L59 · superadminStatus calls (conditional paths may differ): where, from, db.select, eq, configuredEmail.toLowerCase, admins.some
- L73 · isUniqueConstraintError calls (conditional paths may differ): String, test
- L85 · createPersonalSpace calls (conditional paths may differ): slugifyHandle, RESERVED_SLUGS.has, Array.from, createSpace, email.split, isUniqueConstraintError
- L109 · bootstrapSuperadminByEmail calls (conditional paths may differ): rawEmail.toLowerCase, limit, where, from, db.select, eq, set, db.update, toSessionUser, crypto.randomUUID, values, db.insert, createPersonalSpace
- L137 · createSpace calls (conditional paths may differ): crypto.randomUUID, db.batch, values, db.insert
- L149 · isSpaceMember calls (conditional paths may differ): limit, where, from, db.select, and, eq
- L166 · directShareStmt calls (conditional paths may differ): where, from, db.select, and, eq
- L175 · groupReachStmt calls (conditional paths may differ): where, innerJoin, from, db.select, eq, and
- L185 · resolveIsShared calls (conditional paths may differ): batchAll, limit, directShareStmt, groupReachStmt
- L199 · resolveShareRole calls (conditional paths may differ): limit, directShareStmt
- L210 · memberSpaceIdsStmt calls (conditional paths may differ): where, from, db.select, eq
- L224 · sharedSiteRoles calls (conditional paths may differ): batchAll, sharedSiteRoleStmts, foldSharedSiteRoles
- L232 · sharedSiteRoleStmts calls (conditional paths may differ): directShareStmt, groupReachStmt
- L238 · foldSharedSiteRoles calls (conditional paths may differ): roles.set
- L249 · foldMemberSpaceIds calls (conditional paths may differ): rows.map
- L274 · listMentionableUsers calls (conditional paths may differ): orderBy, where, from, db.select, ne, eq, innerJoin, Promise.all, ids.add, ids.delete, inArray
- L326 · listSiteShares calls (conditional paths may differ): where, from, db.select, eq, u.map, g.map
- L346 · replaceSiteShares calls (conditional paths may differ): batchAll, where, db.delete, eq, users.map, values, db.insert, groupIds.map
- L27 · purgeRetention calls (conditional paths may differ): cutoff, then, where, from, db.select, count, and, eq, lt, Number, onConflictDoUpdate, values, db.insert, db.delete, isNotNull
- L61 · scalarCount calls (conditional paths may differ): Number
- L72 · computeTotals calls (conditional paths may differ): Promise.all, scalarCount, from, db.select, count, then, Number, where, isNull, eq
- L108 · computeWindow calls (conditional paths may differ): now.getTime, dayKey, Promise.all, then, where, from, db.select, and, eq, gte, Number, groupBy, count, isNull, limit, orderBy, desc, buildSeries, topSites.map
- L233 · readEntry calls (conditional paths may differ): kv.get, JSON.parse
- L252 · cachedHalf calls (conditional paths may differ): compute, kv.put, JSON.stringify, now.getTime, readEntry, defer, catch, then, write
- L288 · cachedStats calls (conditional paths may differ): Promise.all, cachedHalf, computeTotals, computeWindow
- L304 · buildSeries calls (conditional paths may differ): Object.entries, rows.map, Number, dayKey, now.getTime, out.push, index.signups.get, index.sites.get, index.views.get, index.comments.get
- L8 · isGoogleEnabled calls (conditional paths may differ): Boolean
- L15 · createGoogle calls (conditional paths may differ): isGoogleEnabled
Environment references: env.GOOGLE_CLIENT_ID · env.GOOGLE_CLIENT_SECRET · env.APP_URL
- L19 · admin.use("*")
- L23 · admin.get("/sites")
- L62 · admin.patch("/sites/:id/archive")
- L72 · admin.patch("/sites/:id/restore")
- L83 · admin.delete("/sites/:id")
- L94 · admin.get("/spaces")
- L113 · admin.get("/users")
- L132 · admin.post("/users/:id/revoke-cli")
- L145 · admin.get("/stats")
Environment references: c.env.GLANCE_FILES · c.env.GLANCE_SESSIONS
- L70 · ask.use("*")
- L72 · ask.post("/:space/:site/ask")
- L39 · validateBody calls (conditional paths may differ): question.trim
- L52 · gated calls (conditional paths may differ): c.get, c.req.param, limit, where, innerJoin, from, db.select, eq, and, fetchAccessFacts, siteAccessFromFacts, c.json
Environment references: c.env.ASK_LIMITER
- L46 · auth.get("/google")
- L64 · auth.get("/callback")
- L99 · auth.post("/logout")
- L120 · auth.get("/me")
- L133 · auth.post("/dev-login")
- L157 · auth.post("/bootstrap")
- L229 · auth.post("/cli/start")
- L247 · auth.get("/cli/poll")
- L258 · auth.post("/cli/approve")
- L25 · safeNext calls (conditional paths may differ): next.startsWith
- L198 · generateUserCode calls (conditional paths may differ): crypto.getRandomValues
- L216 · isCliStartRateLimited calls (conditional paths may differ): Number, kv.get, kv.put, String
- L277 · findOrCreateUser calls (conditional paths may differ): limit, where, from, db.select, eq, sanitizeAvatarUrl, set, db.update, toSessionUser, crypto.randomUUID, env.SUPERADMIN_EMAIL.toLowerCase, values, db.insert, createPersonalSpace
Environment references: c.env.ALLOWED_HD · c.env.SESSION_SECRET · c.env.APP_URL · c.env.SUPERADMIN_EMAIL · c.env.GLANCE_SESSIONS · c.env.BOOTSTRAP_TOKEN · env.SUPERADMIN_EMAIL
- L437 · comments.use("*")
- L448 · comments.get("/:space/:site/comments")
- L469 · comments.get("/:space/:site/mentionable")
- L481 · comments.get("/:space/:site/comments/audio/:commentId")
- L525 · comments.get("/:space/:site/comments/socket")
- L563 · comments.post("/:space/:site/comments")
- L660 · comments.post("/:space/:site/comments/:threadId/replies")
- L713 · comments.patch("/:space/:site/comments/:threadId")
- L728 · comments.patch("/:space/:site/comments/:threadId/messages/:commentId")
- L752 · comments.delete("/:space/:site/comments/:threadId/messages/:commentId")
- L826 · comments.put("/:space/:site/comments/:threadId/messages/:commentId/reactions")
- L847 · comments.delete("/:space/:site/comments/:threadId/messages/:commentId/reactions")
- L91 · stripControlChars calls (conditional paths may differ): s.replace
- L110 · siteFromFacts calls (conditional paths may differ): siteAccessFromFacts, c.get, c.json
- L123 · gated calls (conditional paths may differ): c.req.param, fetchAccessFacts, c.get, siteFromFacts
- L144 · siteWithUrlThread calls (conditional paths may differ): c.req.param, gated, threadByIdStmt, c.get
- L159 · siteWithUrlComment calls (conditional paths may differ): c.get, c.req.param, gated, commentByIdStmt, threadByIdStmt, c.json, threadInSite, rows.slice
- L182 · cleanBody calls (conditional paths may differ): trim, stripControlChars
- L195 · notifyForComment calls (conditional paths may differ): c.get, c.req.param, fireAndForget, Array.isArray, opts.rawMentions.filter, map, listMentionableUsers, requested.filter, allowed.has, resolveCommentAudience, mentionRecipients.map, truncateSnippet, createNotifications, audience.map, toRow, deliverSlackForComment
- L277 · deliverSlackForComment calls (conditional paths may differ): slackEnabled, c.get, usersEmailsByIds, audience.map, emails.get, deliverSlack, slackDepsFromEnv
- L306 · pushThreadCreated calls (conditional paths may differ): c.get, buildThreadCreatedView, notifyCommentEvent
- L325 · pushCommentCreated calls (conditional paths may differ): c.get, buildCommentCreatedView, notifyCommentEvent
- L350 · parseThreadFields calls (conditional paths may differ): tooLong, normalizeText, stripControlChars, parseElementAnchor, parseTextContext
- L387 · jsonField calls (conditional paths may differ): JSON.parse
- L401 · ingestVoiceComment calls (conditional paths may differ): form.get, c.json, audioExtFromPart, part.arrayBuffer, crypto.randomUUID, Promise.all, transcribeVoice, c.env.GLANCE_FILES.put, slice, trim, stripControlChars
- L601 · createVoiceThread calls (conditional paths may differ): catch, c.req.formData, c.json, jsonField, form.get, parseThreadFields, ingestVoiceComment, createThread, c.get, deleteKeys, pushThreadCreated, notifyForComment
- L685 · replyVoiceComment calls (conditional paths may differ): catch, c.req.formData, c.json, ingestVoiceComment, addComment, c.get, deleteKeys, pushCommentCreated, notifyForComment
- L787 · cleanEmoji calls (conditional paths may differ): trim, stripControlChars
- L798 · reactionTarget calls (conditional paths may differ): c.req.param, siteWithUrlComment, reactionsByCommentStmt, c.get, catch, c.req.json, cleanEmoji, c.json
Environment references: c.env.SLACK_BOT_TOKEN · c.env.AI · c.env.GLANCE_FILES · c.env.SITE_ROOM · c.env.DATA_TOKEN_SECRET
- L90 · summary.use("*")
- L92 · summary.get("/:space/:site/summary")
- L102 · summary.post("/:space/:site/summary")
- L57 · gated calls (conditional paths may differ): c.get, c.req.param, and, eq, limit, where, innerJoin, from, db.select, fetchAccessFacts, siteAccessFromFacts, c.json
Environment references: c.env.SUMMARY_LIMITER · c.env.GLANCE_FILES
- L71 · dataApi.use("*")
- L90 · dataApi.use("*")
- L140 · dataApi.use("*")
- L153 · dataApi.get("/_sync/changes")
- L195 · dataApi.get("/_sync/socket")
- L226 · dataApi.post("/:collection")
- L267 · dataApi.get("/:collection/:docId")
- L285 · dataApi.get("/:collection")
- L306 · dataApi.put("/:collection/:docId")
- L387 · dataApi.delete("/:collection/:docId")
- L512 · dataToken.use("*")
- L523 · dataToken.post("/:space/:site")
- L61 · getDb calls (conditional paths may differ): c.get, sessionDb
- L128 · credential calls (conditional paths may differ): c.req.header, trim, header.slice, isUpgrade, subprotocols
- L409 · docWhere calls (conditional paths may differ): eq
- L417 · readCreatorWhere calls (conditional paths may differ): readsEveryCreator, eq
- L424 · scoped calls (conditional paths may differ): and, docWhere, eq
- L432 · atSiteDocQuota calls (conditional paths may differ): where, from, db.select, count, eq
- L440 · readJsonBody calls (conditional paths may differ): Number, c.req.header, Number.isFinite, c.req.text, enc.encode, JSON.parse, Array.isArray
- L466 · clampLimit calls (conditional paths may differ): Number, Number.isFinite, Math.min, Math.floor
- L507 · intersectCaps calls (conditional paths may differ): base.filter, ceiling.includes
Environment references: c.env.GLANCE_DB · c.env.DATA_TOKEN_SECRET · c.env.CONTENT_URL · c.env.SITE_ROOM
- L148 · sites.post("/")
- L209 · sites.get("/mine")
- L226 · sites.get("/shared")
- L267 · sites.get("/team")
- L295 · sites.get("/search")
- L317 · sites.get("/:spaceSlug/:siteSlug/exists")
- L339 · sites.get("/:spaceSlug/:siteSlug")
- L482 · sites.get("/:spaceSlug/:siteSlug/shares")
- L501 · sites.put("/:spaceSlug/:siteSlug/shares")
- L547 · sites.patch("/:spaceSlug/:siteSlug")
- L588 · sites.post("/:spaceSlug/:siteSlug/move")
- L646 · sites.post("/:spaceSlug/:siteSlug/fork")
- L759 · sites.delete("/:spaceSlug/:siteSlug")
- L54 · escapeLike calls (conditional paths may differ): s.replace
- L82 · searchSites calls (conditional paths may differ): escapeLike, toLowerCase, q.trim, batchAll, memberSpaceIdsStmt, sharedSiteRoleStmts, foldMemberSpaceIds, keys, foldSharedSiteRoles, eq, or, map, chunk, inArray, as, reaches.map, limit, orderBy, where, innerJoin
- L419 · parseShareGrants calls (conditional paths may differ): Array.isArray, v.filter, groupObjs.some, roles.set, asIds, roles.has, groupObjs.map, map
- L442 · notifyForShare calls (conditional paths may differ): c.get, c.req.param, fireAndForget, createNotifications, recipientIds.map, slackEnabled, usersEmailsByIds, emails.get, deliverSlack, slackDepsFromEnv
- L629 · freeForkSlug calls (conditional paths may differ): map, where, from, db.select, eq, taken.has, isValidSlug
Environment references: c.env.APP_URL · c.env.CONTENT_URL · c.env.CONTENT_TOKEN_SECRET · c.env.SLACK_BOT_TOKEN · c.env.GLANCE_FILES
Build and deployment pipeline · 3 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: push, pull_request
build · no job dependencies declared
- actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - Install dependencies
bun install --frozen-lockfile - Generate Cloudflare types
bun run cf-typegen - Typecheck
bun run typecheck - Build web
bun run build:web - Test
bun run test - Lint
bun run lint
cli · no job dependencies declared
- actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e - Vet + test
test -z "$(gofmt -l .)" || { echo "gofmt drift:"; gofmt -l .; exit 1; } go vet ./... go test ./... - Build sanity (all release targets)
for t in "darwin arm64" "darwin amd64" "linux arm64" "linux amd64"; do set -- $t CGO_ENABLED=0 GOOS="$1" GOARCH="$2" go build -o /dev/null . || exit 1 done
Triggers: push
deploy · no job dependencies declared
- actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 - Install dependencies
bun install --frozen-lockfile - Wire Cloudflare binding IDs into config
: "${CF_ACCOUNT_ID:?set repo Actions variable CF_ACCOUNT_ID}" : "${CF_D1_DATABASE_ID:?set repo Actions variable CF_D1_DATABASE_ID}" : "${CF_KV_NAMESPACE_ID:?set repo Actions variable CF_KV_NAMESPACE_ID}" : "${CF_WORKERS_SUBDOMAIN:?set repo Actions variable CF_WORKERS_SUBDOMAIN}" for f in packages/api/wrangler.jsonc packages/api/wrangler.content.jsonc packages/web/public/_headers; do sed -i \ -e "s/YOUR_ACCOUNT_ID/${CF_ACCOUNT_ID}/g" \ -e "s/YOUR_D1_DATABASE_ID/${CF_D1_DATABASE_ID}/g" \ -e "s/YO… - Build web
bun run build:web - Regenerate committed bundles
bun run build:annotate bun run build:db bun run build:install bun run build:whatsnew bun run build:themes - Apply D1 migrations (remote)
bunx wrangler d1 migrations apply glance-db --remote - Ensure D1 read replication is enabled
out="$(curl -sS -X PUT "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/d1/database/${CF_D1_DATABASE_ID}" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ -H "Content-Type: application/json" \ -d '{"read_replication": {"mode": "auto"}}')" echo "$out" | grep -q '"success": *true' || { echo "$out" >&2; exit 1; } - Deploy main worker
: "${CF_ALLOWED_HD:?set repo Actions variable CF_ALLOWED_HD}" : "${CF_SUPERADMIN_EMAIL:?set repo Actions variable CF_SUPERADMIN_EMAIL}" bunx wrangler deploy \ --var "APP_URL:${APP_URL}" \ --var "CONTENT_URL:${CONTENT_URL}" \ --var "ALLOWED_HD:${CF_ALLOWED_HD}" \ --var "SUPERADMIN_EMAIL:${CF_SUPERADMIN_EMAIL}" - Deploy content worker
bunx wrangler deploy --config wrangler.content.jsonc --var "APP_URL:${APP_URL}"
Triggers: push
build · no job dependencies declared
- actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e - Build standalone binaries
mkdir -p dist # Stamp the release tag (v1.2.3 -> 1.2.3) into the binary via -ldflags -X so it reports the # exact release version in its User-Agent (drives CLI-usage analytics) and self-update compare. v="${GITHUB_REF_NAME#v}" while read -r goos goarch out; do CGO_ENABLED=0 GOOS="$goos" GOARCH="$goarch" \ go build -trimpath -ldflags="-s -w -X main.version=$v" -o "dist/$out" . # Checksum the uncompressed binary (what actually runs), then gzip to shrink the download. # The installer + self-update… - Publish release
softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228
build:web: cd packages/web && bunx vite builddeploy: bun run build:web && cd packages/api && bunx wrangler deploy && bunx wrangler deploy --config wrangler.content.jsonc
build:annotate: bun scripts/build-annotate.tsbuild:db: bun scripts/build-db.tsbuild:themes: bun scripts/build-themes.tsbuild:install: bun scripts/build-install.tsbuild:whatsnew: bun scripts/build-whatsnew.tsdeploy: wrangler deploydeploy:content: bun run build:annotate && bun run build:db && bun run build:themes && wrangler deploy --config wrangler.content.jsonc
build: vite build
Repository README
View original on GitHub ↗Full upstream document by @plivo-labs · README.md · snapshot ff602b1
Glance
Self-hosted artifacts for any coding agent. Your agent builds a page, dashboard, or app and publishes it to a live URL with one command. You leave comments in the browser, like in a Google Doc. The agent reads them and fixes the page.
Works with Claude Code, Cursor, Codex, Cline, Aider, or anything else that can run a shell command. Runs on Cloudflare's free tier.
agent builds → glance deploy → URL
↑ ↓
reads comments, fixes ← you comment in the browser
Self-host
You need a Cloudflare account with R2 turned on (dashboard → R2 → accept the terms; it's still free). Then run:
bun install
bunx wrangler login
scripts/setup.sh
setup.sh creates the resources, deploys the app, and prints your URL plus a bootstrap token. Open the printed /login page and paste the token into Complete setup to become the first admin. You can run the script again safely.
Using more than one Cloudflare account? Run export CLOUDFLARE_ACCOUNT_ID=<id> first. For manual setup or Google SSO, see DEPLOY.md.
Use it
curl -fsSL https://<your-instance>/api/install | sh # installs the CLI and the agent skill
glance login
glance deploy ./my-report # file or folder → live URL
glance comments <space/slug> # read review comments
Run glance with no arguments to see every command.
The install script also adds the agent skill for Claude Code. For other agents, run:
npx skills add plivo-labs/glance
CI: create an API key at /settings/keys and set GLANCE_TOKEN=glk_.... For the HTTP API, see packages/api/API.md.
Features
- Visibility: each site is
private,members, orteam. Every link requires a login; nothing is public. - Audio: audio files play in a built-in player. You can record audio or leave voice comments in the browser. Voice comments are transcribed, so agents read them as text.
- Slack (optional): get comment notifications as Slack DMs, and Glance links show as preview cards in Slack. See DEPLOY.md.
glance.db(experimental, opt-in): a small JSON document store your pages can use directly from the browser. See SHARED_BACKEND.md.
Security
Uploaded HTML and JS are treated as untrusted. They are served from a separate domain, so they can't read your login session. That's why Glance runs two Workers. To report a vulnerability, see SECURITY.md.
Development
Built with Cloudflare Workers + Hono, React Router v7, D1, R2, and KV. The CLI is written in Go.
packages/api Worker: API + file serving
packages/web React app
packages/cli glance CLI
For local setup and checks, see CONTRIBUTING.md.
License
Frequently asked about Glance
What is Glance?+
Glance is a self-hosted Claude Artifacts alternative built on the Cloudflare developer platform. Publish agent-created pages and review them through shareable links and comments.
What does Glance replace?+
Glance is listed as an alternative to Claude Artifacts. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does Glance use?+
Glance is built on D1, Durable Objects, KV, R2, Workers, Workers AI.
How much does Glance cost to run?+
The documented Glance deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Keep aggregate D1 use below 5 million rows read/day, 100,000 rows written/day and 5 GB total storage; a request can touch many rows. Keep KV below 100,000 reads/day, 1,000 writes, deletes and list operations/day each, and 1 GB; cache refreshes and backups consume writes. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Use the configured SQLite Durable Object classes within 100,000 requests/day, 13,000 GB-s duration/day, 5 million SQL rows read/day, 100,000 written/day and 5 GB storage; active sockets consume duration. Only ordinary Free-eligible Workers AI models are covered, within 10,000 neurons/day; optional external providers and paid-only models are excluded. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is Glance open source?+
The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/plivo-labs/glance/ff602b1974274b39be719f82833e0aefd1ed3acc/LICENSE. Source code and contributor credit are available at https://github.com/plivo-labs/glance.

Discussion · 0
sign in to comment →