
ShareHTML
Publish HTML or Markdown documents and discuss them through a shared link.
ShareHTML is a self-hosted Netlify Drop alternative built on Cloudflare (Durable Objects, R2, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.
Source & license
Upstream license: Apache-2.0
License TL;DR
You can use, change and sell it, including in closed-source products. When sharing copies, include the license, keep required notices and mark changed files. It includes a contributor patent grant with conditions, but no trademark permission or warranty.
Explain Apache 2.0 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 ShareHTML 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.
- 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.
- 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: 27ddaaf. Hosting eligibility reflects the deployment documentation and listed assumptions.
- netlify-drop ↗
- **CLI deploys** — `sharehtml deploy report.html` → `https://sharehtml.yourteam.workers.dev/d/9brkzbe67ntm` - **Collaborative** — comments, threaded replies, emoji reactions, text anchoring - **Live presence** — see who's viewing and their selections - **Home page** — your documents and recently viewed docs shared with you - **Self-hosted** — runs on your own Cloudflare account
- workers ↗
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "sharehtml-dev", "main": "src/index.ts", "compatibility_date": "2025-04-01", "workers_dev": false, "preview_urls": false, "vars": { "AUTH_MODE": "none", }, "assets": { "not_found_handling": "none", "binding": "ASSETS", "run_worker_first": ["/d/*", "/api/*", "/health", "/"], }, "r2_buckets": [ { "binding": "DOCUMENTS_BUCKET",
- r2 ↗
andling": "none", "binding": "ASSETS", "run_worker_first": ["/d/*", "/api/*", "/health", "/"], }, "r2_buckets": [ { "binding": "DOCUMENTS_BUCKET", "bucket_name": "sharehtml-documents-dev", }, ], "durable_objects": { "bindings": [ { "name": "DOCUMENT_DO", "class_name": "DocumentDO", }, { "name": "REGISTRY_DO", "class_name": "RegistryDO", }, ], }, "upload_source_maps": true, "observability": { "logs": {
- durable-objects ↗
g": "DOCUMENTS_BUCKET", "bucket_name": "sharehtml-documents-dev", }, ], "durable_objects": { "bindings": [ { "name": "DOCUMENT_DO", "class_name": "DocumentDO", }, { "name": "REGISTRY_DO", "class_name": "RegistryDO", }, ], }, "upload_source_maps": true, "observability": { "logs": { "enabled": true, "invocation_logs": true, }, "traces": { "enabled": true, }, }, "migrations": [ {
- free-tier-eligible ↗
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "sharehtml-dev", "main": "src/index.ts", "compatibility_date": "2025-04-01", "workers_dev": false, "preview_urls": false, "vars": { "AUTH_MODE": "none", }, "assets": { "not_found_handling": "none", "binding": "ASSETS", "run_worker_first": ["/d/*", "/api/*", "/health", "/"], }, "r2_buckets": [ { "binding": "DOCUMENTS_BUCKET",
- free-tier-eligible ↗
andling": "none", "binding": "ASSETS", "run_worker_first": ["/d/*", "/api/*", "/health", "/"], }, "r2_buckets": [ { "binding": "DOCUMENTS_BUCKET", "bucket_name": "sharehtml-documents-dev", }, ], "durable_objects": { "bindings": [ { "name": "DOCUMENT_DO", "class_name": "DocumentDO", }, { "name": "REGISTRY_DO", "class_name": "RegistryDO", }, ], }, "upload_source_maps": true, "observability": { "logs": {
- free-tier-eligible ↗
g": "DOCUMENTS_BUCKET", "bucket_name": "sharehtml-documents-dev", }, ], "durable_objects": { "bindings": [ { "name": "DOCUMENT_DO", "class_name": "DocumentDO", }, { "name": "REGISTRY_DO", "class_name": "RegistryDO", }, ], }, "upload_source_maps": true, "observability": { "logs": { "enabled": true, "invocation_logs": true, }, "traces": { "enabled": true, }, }, "migrations": [ {
- 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 ↗
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 ↗
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(
- Apache-2.0 ↗
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to ca
- architecture ↗
{ "$schema": "./node_modules/wrangler/config-schema.json", "name": "sharehtml-dev", "main": "src/index.ts", "compatibility_date": "2025-04-01", "workers_dev": false, "preview_urls": false, "vars": { "AUTH_MODE": "none", }, "assets": { "not_found_handling": "none", "binding": "ASSETS", "run_worker_first": ["/d/*", "/api/*", "/health", "/"], }, "r2_buckets": [ { "binding": "DOCUMENTS_BUCKET",
- architecture ↗
andling": "none", "binding": "ASSETS", "run_worker_first": ["/d/*", "/api/*", "/health", "/"], }, "r2_buckets": [ { "binding": "DOCUMENTS_BUCKET", "bucket_name": "sharehtml-documents-dev", }, ], "durable_objects": { "bindings": [ { "name": "DOCUMENT_DO", "class_name": "DocumentDO", }, { "name": "REGISTRY_DO", "class_name": "RegistryDO", }, ], }, "upload_source_maps": true, "observability": { "logs": {
- architecture ↗
g": "DOCUMENTS_BUCKET", "bucket_name": "sharehtml-documents-dev", }, ], "durable_objects": { "bindings": [ { "name": "DOCUMENT_DO", "class_name": "DocumentDO", }, { "name": "REGISTRY_DO", "class_name": "RegistryDO", }, ], }, "upload_source_maps": true, "observability": { "logs": { "enabled": true, "invocation_logs": true, }, "traces": { "enabled": true, }, }, "migrations": [ {
Upstream screenshot · jonesphillip/sharehtml repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Publishing static HTML or Markdown documents to a shareable URL; Netlify build pipelines, backend platform services and full deployment tooling are excluded.
See supporting source ↗How it works
The shape of ShareHTML 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 27ddaaf191b5. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 1 files
Cloudflare Workers · compatibility 2025-04-01
sharehtml-dev · default
Entrypoint: src/index.ts
Static assets: directory not declared · none · Worker first: ["/d/*","/api/*","/health","/"]
DOCUMENTS_BUCKET→ R2DOCUMENT_DO→ Durable Objects · class DocumentDOREGISTRY_DO→ Durable Objects · class RegistryDOASSETS→ Static assets
sharehtml · env.production
Inherited from default: main, compatibility_date
Entrypoint: src/index.ts
Static assets: directory not declared · none · Worker first: ["/d/*","/api/*","/health","/"]
DOCUMENTS_BUCKET→ R2DOCUMENT_DO→ Durable Objects · class DocumentDOREGISTRY_DO→ Durable Objects · class RegistryDOASSETS→ Static assets
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.
- L34 · app.get("/health")
- L36 · app.use("/*")
- L38 · app.route("/api")
- L39 · app.route("/")
- L41 · app.get("/")
Environment references: c.env.ASSETS · c.env.AUTH_MODE
- L17 · getJWKS calls (conditional paths may differ): createRemoteJWKSet
- L29 · getAccessJWT calls (conditional paths may differ): c.req.header, getCookie
- L48 · verifyAccessJWT calls (conditional paths may differ): console.error, getJWKS, jwtVerify, String
Environment references: env.ACCESS_AUD · env.ACCESS_TEAM · c.env.AUTH_MODE
- L167 · api.post("/documents")
- L233 · api.get("/documents/by-filename")
- L257 · api.get("/documents/recent")
- L268 · api.get("/documents")
- L297 · api.get("/documents/:id/raw")
- L328 · api.get("/documents/:id/source")
- L352 · api.get("/documents/:id/rendered")
- L376 · api.get("/documents/:id")
- L388 · api.put("/documents/:id")
- L528 · api.get("/documents/:id/share")
- L542 · api.put("/documents/:id/share")
- L580 · api.delete("/documents/:id")
- L616 · api.get("/documents/:id/comments")
- L20 · inferSourceKind calls (conditional paths may differ): test
- L32 · getDocumentTitle calls (conditional paths may differ): inferSourceKind, filename.replace
- L61 · narrowSourceKind calls (conditional paths may differ): isSourceKind
- L65 · parseSourceFields calls (conditional paths may differ): formData.get, isSourceKind
- L83 · parseShareRequestBody calls (conditional paths may differ): isRecord, isShareMode, Array.isArray, entry.includes, emails.push
- L123 · migrateDocumentAnchors calls (conditional paths may differ): documentDo.fetch, JSON.stringify
- L140 · getDocumentSnapshot calls (conditional paths may differ): documentDo.fetch, parseDocumentSnapshot, response.json
- L152 · restoreDocumentSnapshot calls (conditional paths may differ): documentDo.fetch, JSON.stringify
Environment references: c.env.DOCUMENTS_BUCKET · c.env.AUTH_MODE · c.env.DOCUMENT_DO
- L17 · viewer.get("/d/:id")
- L53 · viewer.post("/d/:id/capability")
- L73 · viewer.get("/d/:id/content")
- L100 · viewer.get("/d/:id/ws")
Environment references: c.env.ASSETS · c.env.AUTH_MODE · c.env.DOCUMENTS_BUCKET · c.env.DOCUMENT_DO
- L33 · encodeBase64Url calls (conditional paths may differ): String.fromCharCode, replace, btoa
- L39 · decodeBase64Url calls (conditional paths may differ): replace, value.replace, repeat, atob, binary.charCodeAt
- L54 · importHmacKey calls (conditional paths may differ): hmacKeyCache.get, crypto.subtle.importKey, encode, hmacKeyCache.set
- L68 · sign calls (conditional paths may differ): importHmacKey, crypto.subtle.sign, encode, encodeBase64Url
- L74 · verify calls (conditional paths may differ): importHmacKey, decodeBase64Url, crypto.subtle.verify, encode
- L81 · parsePayload calls (conditional paths may differ): JSON.parse, isRecord, Number.isFinite
- L104 · createCapabilityToken calls (conditional paths may differ): normalizeEmail, Math.floor, Date.now, crypto.randomUUID, encodeBase64Url, encode, JSON.stringify, sign, getCapabilitySecret
- L131 · verifyCapabilityToken calls (conditional paths may differ): token.split, getCapabilitySecret, verify, decodeBase64Url, parsePayload, decode, normalizeEmail, Math.floor, Date.now
Environment references: env.VIEWER_CAPABILITY_SECRET · env.AUTH_MODE
- L16 · getAssetUrls calls (conditional paths may differ): assets.fetch, resp.json
Environment references: env.DEV
- L1 · getRegistry calls (conditional paths may differ): env.REGISTRY_DO.get, env.REGISTRY_DO.idFromName
Environment references: env.REGISTRY_DO
- L18 · isClientMessage calls (conditional paths may differ): isRecord, clientMessageTypes.has
- L29 · parseAttachment calls (conditional paths may differ): isRecord
- L45 · parseVerifiedEmail calls (conditional paths may differ): isRecord
- L50 · getSelectedWebSocketProtocol calls (conditional paths may differ): filter, map, header.split, value.trim, protocols.includes
- L3 · nanoid calls (conditional paths may differ): crypto.getRandomValues
- L4 · extractDocumentTextFromHtml calls (conditional paths may differ): html.replace, text, transform, on
- L45 · getCapabilityFromRequest calls (conditional paths may differ): c.req.header, getCapabilityFromWebSocketProtocol
- L55 · getCapabilityFromWebSocketProtocol calls (conditional paths may differ): filter, map, header.split, value.trim, protocol.startsWith, protocol.slice
- L68 · originHostMatchesRequest calls (conditional paths may differ): c.req.header
- L85 · createForbiddenResponse calls (conditional paths may differ): c.text, c.json
- L96 · logSecurityWarning calls (conditional paths may differ): console.warn, toISOString
- L111 · requireBrowserCapability calls (conditional paths may differ): c.get, isExplicitHeaderAuthSource, c.req.header, originHostMatchesRequest, logSecurityWarning, createForbiddenResponse, isCookieAuthSource, getCapabilityFromRequest, verifyCapabilityToken
- L168 · requireHomeBrowserCapability calls (conditional paths may differ): requireBrowserCapability, isUnsafeMethod
- L181 · requireViewerBrowserCapability calls (conditional paths may differ): requireBrowserCapability, isUnsafeMethod
- L15 · getTextQuoteSelector calls (conditional paths may differ): anchor.selectors.find
- L23 · getTextPositionSelector calls (conditional paths may differ): anchor.selectors.find
- L33 · getElementSelector calls (conditional paths may differ): anchor.selectors.find
- L41 · buildTextQuoteSelector calls (conditional paths may differ): text.slice, Math.max
- L62 · rebuildAnchor calls (conditional paths may differ): anchor.selectors.filter, buildTextPositionSelector, buildTextQuoteSelector
- L76 · findAnchorRangeInText calls (conditional paths may differ): getTextQuoteSelector, getTextPositionSelector, isValidPositionMatch, findStrictQuoteMatch
- L91 · findStrictQuoteMatch calls (conditional paths may differ): text.indexOf, Boolean, text.slice, Math.max, allMatches.push, perfectMatches.push, oneSidedMatches.push
- L144 · isValidPositionMatch calls (conditional paths may differ): text.slice
- L38 · collectAnnotatableElementsFromHtml calls (conditional paths may differ): html.replace, element.tagName.toLowerCase, ordinals.get, ordinals.set, elements.push, element.getAttribute, parseNumberAttribute, VOID_TAGS.has, stack.push, element.onEndTag, stack.pop, text, transform, on
- L85 · rebuildElementAnchor calls (conditional paths may differ): anchor.selectors.filter, buildElementSelector
- L98 · remapElementAnchor calls (conditional paths may differ): getElementSelector, nextElements.find, matchesElementSignature, rebuildIfChanged, findStrongElementMatches
- L135 · rebuildIfChanged calls (conditional paths may differ): rebuildElementAnchor, JSON.stringify
- L146 · findStrongElementMatches calls (conditional paths may differ): elements.filter, matchesElementSignature, signatureMatches.filter
- L202 · parseNumberAttribute calls (conditional paths may differ): Number.parseInt, Number.isFinite
- L11 · diffText calls (conditional paths may differ): getCommonPrefixLength, oldText.slice, newText.slice, getCommonSuffixLength, operations.push, diffMiddle, mergeDiffOps
- L42 · mapRangeThroughDiff calls (conditional paths may differ): rangesOverlap, mapPositionThroughDiff
- L119 · diffMiddle calls (conditional paths may differ): buildMyersDiff
- L131 · buildMyersDiff calls (conditional paths may differ): frontier.set, trace.push, frontier.get, oldText.charCodeAt, newText.charCodeAt, backtrackMyers
- L178 · backtrackMyers calls (conditional paths may differ): frontier.get, operations.push, oldText.charAt, newText.charAt, mergeDiffOps, operations.reverse
- L223 · mergeDiffOps calls (conditional paths may differ): merged.push
- L237 · getCommonPrefixLength calls (conditional paths may differ): Math.min, oldText.charCodeAt, newText.charCodeAt
- L246 · getCommonSuffixLength calls (conditional paths may differ): Math.min, oldText.charCodeAt, newText.charCodeAt
Build and deployment pipeline · 0 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.
No GitHub Actions workflow was found in the collected tree. Deployment may be manual or configured elsewhere.
deploy: pnpm --filter @sharehtml/worker run deploybuild: pnpm -r build
build: bun build src/index.ts --outdir dist --target bun
build: vite builddeploy: CLOUDFLARE_ENV=production vite build && wrangler deploy --env production
build: tsc
Repository README
View original on GitHub ↗Full upstream document by @jonesphillip · README.md · snapshot 27ddaaf
sharehtml
I've been using coding agents to write in markdown, make slides, and build interactive data analysis as static HTML files. Sending those files around still gets messy fast: you can't update them after sharing, and there's no way to get feedback inline. This is the reason I built sharehtml.

What is sharehtml?
Deploy a local document, get a link where others can view it and collaborate with comments, reactions, and live presence. Re-deploy to update the content at the same URL. Markdown and common code files are converted to styled HTML automatically.
- CLI deploys —
sharehtml deploy report.html→https://sharehtml.yourteam.workers.dev/d/9brkzbe67ntm - Collaborative — comments, threaded replies, emoji reactions, text anchoring
- Live presence — see who's viewing and their selections
- Home page — your documents and recently viewed docs shared with you
- Self-hosted — runs on your own Cloudflare account
Prerequisites
- Node.js 18+ and pnpm
- Bun (for the CLI and setup script)
- Cloudflare account with R2 enabled (free tier available)
Quick Start
git clone https://github.com/jonesphillip/sharehtml.git
cd sharehtml
pnpm install
npx wrangler login
pnpm run setup
The interactive setup script walks you through everything: deploying the worker, installing the CLI, and configuring authentication. Cloudflare Access is optional — the setup script asks if you want authentication. Without it, anyone with a link can view and comment.
If you enable Cloudflare Access, setup also provisions the production VIEWER_CAPABILITY_SECRET used to sign browser capability tokens for the trusted viewer shell.
To install the CLI directly:
# with Bun
bun install -g sharehtml
# or with npm (Bun still needs to be installed for the CLI runtime)
npm install -g sharehtml
If your team already has a sharehtml worker deployed, this is probably all you need — install the CLI, run sharehtml config set-url <your-team-url>, then sharehtml login.
If you enable Cloudflare Access, you'll need a Cloudflare API token with these permissions:
- Account > Access: Apps and Policies > Edit
- Account > Access: Organization, Identity Providers, and Groups > Read
- Account > Workers Scripts > Read (to resolve your workers.dev subdomain)
When it's done, try deploying one of the included examples:
sharehtml deploy example/coffee-report.html
# or try the markdown example:
sharehtml deploy example/sample.md
# or the interactive slideshow example:
sharehtml deploy example/nba-slideshow.html
# or deploy a code file:
sharehtml deploy apps/cli/src/index.ts
If a document with the same filename exists, the CLI will prompt to update it. Use -u to skip the prompt.
Manual deploy
If you've already run setup and just need to redeploy:
pnpm run deploy
If AUTH_MODE=access, make sure the production worker already has VIEWER_CAPABILITY_SECRET configured. pnpm run setup handles that automatically.
To create it manually:
openssl rand -hex 32
npx wrangler secret put VIEWER_CAPABILITY_SECRET --env production
Local development
pnpm dev
Starts the Vite dev server with Wrangler at http://localhost:5173. Local dev uses the default environment — AUTH_MODE is "none", no login required.
Local development does not require VIEWER_CAPABILITY_SECRET.
To use the CLI locally:
sharehtml config set-url http://localhost:5173
sharehtml deploy my-report.html
Architecture
CLI ──► Worker ──► R2 (HTML storage)
│
Browser ◄┘──► Durable Objects
├── RegistryDO (users, documents, views)
└── DocumentDO (per-doc comments, reactions, presence via WebSocket)
| Component | Purpose |
|---|---|
| Worker | HTTP routing, auth, serves viewer shell and home page |
| RegistryDO | Global Durable Object — users, document metadata, view history (SQLite) |
| DocumentDO | Per-document Durable Object — comments, reactions, real-time presence over WebSocket |
| R2 | Stores the actual HTML files |
| CLI | Bun-based command-line tool for deploying and managing documents |
CLI Commands
| Command | Description |
|---|---|
sharehtml deploy <file> |
Deploy an HTML, Markdown, or code file (creates or updates) |
sharehtml list |
List your documents |
sharehtml open <id> |
Open a document in the browser |
sharehtml pull <id> |
Download a document locally |
sharehtml diff <file> |
Compare local file against the deployed version |
sharehtml comments <id> |
Show unresolved comments for a document |
sharehtml delete <id> |
Delete a document |
sharehtml share <document> |
Share by link, or --add/--remove emails |
sharehtml unshare <document> |
Make a document private |
sharehtml skill install |
Install the agent skill for Claude Code, Codex, or OpenCode |
sharehtml login |
Log in through Cloudflare Access |
sharehtml config set-url <url> |
Set the sharehtml URL |
sharehtml config show |
Show current configuration |
Agent Skill
Install the sharehtml skill to let coding agents deploy documents, compare changes, and review comments on your behalf. The skill teaches agents to diff before overwriting and keep documents private by default. Run sharehtml skill install to set it up for Claude Code, Codex, or OpenCode.
Configuration
Production auth vars live in wrangler.jsonc under env.production.vars, set by pnpm run setup:
| Variable | Required | Description |
|---|---|---|
AUTH_MODE |
Yes | "none" disables auth, "access" enables Cloudflare Access JWT verification |
ACCESS_AUD |
When AUTH_MODE=access |
Cloudflare Access Application Audience tag |
ACCESS_TEAM |
When AUTH_MODE=access |
Cloudflare Access team name |
Production secrets:
| Secret | Required | Description |
|---|---|---|
VIEWER_CAPABILITY_SECRET |
When AUTH_MODE=access |
Wrangler secret used to sign short-lived browser capability tokens. These tokens let the trusted parent viewer shell call privileged browser endpoints without giving the same authority to untrusted uploaded JS running inside the sandboxed iframe. |
Project Structure
apps/
├── worker/
│ ├── src/
│ │ ├── index.ts # Hono app, routing
│ │ ├── routes/
│ │ │ ├── api.ts # REST API (CRUD documents)
│ │ │ ├── viewer.ts # Document viewer + WebSocket proxy
│ │ ├── durable-objects/
│ │ │ ├── registry.ts # RegistryDO — users, docs, views
│ │ │ └── document.ts # DocumentDO — comments, reactions, presence
│ │ ├── frontend/
│ │ │ ├── home.tsx # Home page (document list)
│ │ │ ├── shell.tsx # Document viewer shell
│ │ ├── client/
│ │ │ ├── shell-client.ts # Viewer shell JS (presence, sidebar)
│ │ │ ├── collab-client.ts # In-iframe collaboration (comments, reactions)
│ │ │ └── styles.css # Shared styles
│ │ └── utils/
│ │ ├── auth.ts # CF Access JWT verification
│ │ ├── registry.ts # getRegistry() helper
│ │ ├── crypto.ts # sha256 utility
│ │ ├── assets.ts # Vite asset URL resolution
│ │ └── ids.ts # nanoid generator
│ ├── scripts/
│ │ └── setup.ts # Interactive production setup script
│ └── wrangler.jsonc # Cloudflare Workers config
├── cli/
│ └── src/
│ ├── index.ts # CLI entry point (commander)
│ ├── commands/ # deploy, list, open, delete, config
│ ├── api/ # HTTP client for worker API
│ └── config/ # Local config store (~/.config/sharehtml)
└── packages/
└── shared/ # Shared types (messages, comments, reactions)
License
Apache-2.0
Frequently asked about ShareHTML
What is ShareHTML?+
ShareHTML is a self-hosted Netlify Drop alternative built on the Cloudflare developer platform. Publish HTML or Markdown documents and discuss them through a shared link.
What does ShareHTML replace?+
ShareHTML is listed as an alternative to Netlify Drop. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does ShareHTML use?+
ShareHTML is built on Durable Objects, R2, Workers.
How much does ShareHTML cost to run?+
The documented ShareHTML 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. 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. 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 ShareHTML open source?+
The upstream repository declares the Apache-2.0 license. Read its terms at https://raw.githubusercontent.com/jonesphillip/sharehtml/27ddaaf191b535346453134fd693e2295a38b88f/LICENSE. Source code and contributor credit are available at https://github.com/jonesphillip/sharehtml.

Discussion · 0
sign in to comment →