Run your own ProtectedShare
Two pieces: a Cloudflare Worker with a D1 database and an R2 bucket for the API, and the Next.js web app pointed at it. Both are in the same MIT-licensed repo.
Web app
Next.js. All encryption runs here, in the visitor's browser. It proxies /api/* to your API.
API
A Hono app on Cloudflare Workers with one D1 database and an R2 bucket for file-drop chunks. Stores ciphertext and metadata only.
Cleanup cron
Runs every 30 minutes and deletes expired shares, drops and their R2 chunks, requests, receipts, idle chat rows and other expired records.
Deploy the API
Needs a Cloudflare account and Node 22. Run these from apps/api.
Create the database and the bucket
Create a D1 database, then put the
database_idit prints intoapps/api/wrangler.toml. Then create the R2 bucket that holds encrypted file-drop chunks. It has to exist before you deploy, or the deploy fails.cd apps/api npx wrangler d1 create protectedshare npx wrangler r2 bucket create protectedshare-dropsRather keep chunks in D1? Delete the
[[r2_buckets]]block fromwrangler.toml; the API notices and stores them in the database. With R2, a lifecycle rule is recommended. It expires anything a delete missed; the cron also sweeps orphaned chunks, but each run scans only the first ~10,000 drops in the bucket:npx wrangler r2 bucket lifecycle add protectedshare-drops expire-old-drops drops/ --expire-days 31Apply the schema
A new database takes the full schema in one go.
schema.sqlalready includes everything from migrations 0001 to 0009.npx wrangler d1 execute protectedshare --remote --file=schema.sqlUpgrading an older install? Apply the migrations you're missing, in order:
0005_v2.sql(receipts, file drops, secret requests, chat rooms),0006_security.sql(download sessions, the request respond hash),0007_r2_limits_stats_reports.sql(usage counts, abuse reports, lockouts, chunk sizes),0008_push.sql(read notifications) and0009_drop_budget.sql(the file-drop spending guard). If one reports a duplicate column, that column is already there and you can move on. The Worker also creates missing tables and columns on first use.npx wrangler d1 execute protectedshare --remote --file=migrations/0007_r2_limits_stats_reports.sql npx wrangler d1 execute protectedshare --remote --file=migrations/0008_push.sql npx wrangler d1 execute protectedshare --remote --file=migrations/0009_drop_budget.sqlDeploy the Worker
wrangler.tomlalready declares the cleanup cron (*/30 * * * *), Workers observability logs, the R2 binding and six rate limiters. Each limiter'snamespace_id(71001 to 71006) must be unique within your Cloudflare account, so change any that clash. Delete a[[ratelimits]]block to skip that check.npx wrangler deployIf your web app uses its built-in
/apiproxy, every request reaches the Worker from your server's IP address, soRL_CREATE(600 new links a minute per IP) becomes a cap for the whole site. Raise it or delete it. A host's rewrite softens this but doesn't remove it: the Worker then sees the host's shared egress IPs. SettingNEXT_PUBLIC_API_ORIGINremoves it, because browsers then call the Worker themselves.Optional Worker secrets:
RESEND_API_KEYemails contact-form messages and abuse reports (without it they're only saved in D1), andADMIN_TOKEN, at least 32 characters, turns onDELETE /api/admin/:kind/:idfor takedowns. The emails go toNOTIFY_EMAIL_TO(default admin@protectedshare.me) fromNOTIFY_EMAIL_DOMAIN(default protectedshare.me), which must be a domain your Resend account has verified. Those two aren't secret: set them under[vars]inwrangler.toml, or as secrets.npx wrangler secret put RESEND_API_KEY npx wrangler secret put ADMIN_TOKENTurn on read notifications (optional)
Generate a VAPID key pair and store it, with a contact address for push services, as Worker secrets. Nothing is written to disk.
npm run vapid:generate npx wrangler secret put VAPID_PUBLIC_KEY npx wrangler secret put VAPID_PRIVATE_KEY npx wrangler secret put VAPID_SUBJECT # e.g. mailto:you@example.comThen build the web app with
NEXT_PUBLIC_VAPID_PUBLIC_KEYset to the same public key. Without the three secrets the API refuses to register browsers; without the web setting the notification bell doesn't appear. Rotating the keys quietly ends existing subscriptions.
Run the web app
Use the published image, or build it yourself.
The image on GitHub Container Registry is the same web app as protectedshare.me, without analytics or read notifications: NEXT_PUBLIC_* settings are baked in when the app is built, and the image is built with none. Point it at your Worker with API_BACKEND_URL, read at runtime.
docker pull ghcr.io/kunalsiyag/protectedshare-web:latest
docker run --rm -p 3000:3000 \
-e API_BACKEND_URL=https://protectedshare-api.<you>.workers.dev \
ghcr.io/kunalsiyag/protectedshare-web:latestTo build your own image from the repo root (put any NEXT_PUBLIC_* values in apps/web/.env.production first):
docker build -f apps/web/Dockerfile -t protectedshare-web .Or deploy apps/web to any Next.js host with the same API_BACKEND_URL, and set the build-time settings below there. Leave them unset to ship without analytics or notifications. On Vercel, note that apps/web/vercel.json rewrites /api straight to protectedshare.me's API Worker. Point that rewrite at your own Worker, or delete the file to use the built-in proxy. Better: give the Worker its own domain and set NEXT_PUBLIC_API_ORIGIN to it, so browsers call it directly and API traffic (file drops included) doesn't count against your web host's bandwidth.
Run the MCP server
Optional. For Claude and other MCP clients.
The remote connector is a second Worker in apps/mcp. It encrypts in memory and passes your API ciphertext and hashes to create a link, and a link's manage token to check or revoke it.
cd apps/mcp
npx wrangler deployBefore deploying, edit apps/mcp/wrangler.toml. Delete the routes block (it claims mcp.protectedshare.me) and use the workers.dev URL or your own domain. Point SITE_URL at your web app. Keep the [[services]] binding if your API Worker is protectedshare-api in the same account; otherwise delete it and set API_BASE_URL (with neither, it calls protectedshare.me's API). ALLOWED_ORIGINS lists the hostnames allowed in an Origin header besides localhost. The optional limiters RL_MCP (600 requests a minute per IP, namespace_id 71101) and RL_MCP_CREATE (120 calls a minute per IP to the tools that create links, 71102) group IPv6 addresses by /64 and must not clash with other limiters in your account. The Worker takes one JSON-RPC message per request and refuses batches.
The local server needs no deploy: point npx protectedshare-mcp at your instance with PROTECTEDSHARE_API_URL (https, except for localhost).
Settings
Everything you can configure.
- API_BACKEND_URL
- Web app, runtime. Base URL of your Worker. The app proxies /api/* to it. If unset, it falls back to the hosted protectedshare.me API, so always set it.
- NEXT_PUBLIC_GA_ID
- Web app, build time, optional. Google Analytics measurement id. Your privacy page states whether it's on.
- NEXT_
PUBLIC_ CF_ BEACON_ TOKEN - Web app, build time, optional. Cloudflare Web Analytics site token: cookieless, full page loads only, never on share pages. Your privacy page states whether it's on.
- NEXT_
PUBLIC_ VAPID_ PUBLIC_ KEY - Web app, build time, optional. The Worker's VAPID public key. Without it, read notifications are off.
- NEXT_
PUBLIC_ GOOGLE_ SITE_ VERIFICATION - Web app, build time, optional. Google Search Console verification code for the site's meta tag.
- NEXT_
PUBLIC_ API_ ORIGIN - Web app, build time, optional. The API Worker's own origin, e.g. https://api.example.com. Browsers then call it directly (it's added to the CSP), so API traffic never passes through the web host. Unset, the app relays /api to API_BACKEND_URL.
- DB
- Worker binding to your D1 database, in wrangler.toml.
- DROPS_BUCKET
- Worker binding to the R2 bucket for file-drop chunks, in wrangler.toml (
[[r2_buckets]]). Delete the block to keep chunks in D1. - DROP_
STORAGE_ LIMIT_ GB, R2_ CLASS_ A_ DAILY_ LIMIT, R2_ CLASS_ B_ DAILY_ LIMIT - Worker variables in wrangler.toml (
[vars]), optional. The spending guard: file drops stop taking new uploads or downloads at these caps instead of running up an R2 bill. Defaults: 9 GB stored, 30,000 R2 writes and 300,000 reads per UTC day, which stay inside R2's free allowance for the month. Raising them can cost money. - RL_AUTH … RL_CREATE
- Worker, in wrangler.toml (
[[ratelimits]]), optional. Per minute: 10 wrong keys or passwords per share, room or notepad (password-protected ones then lock for 10 minutes), 30 room joins and 120 messages per room, 10 answers per request, 5 reports per link (report emails: 5 a minute per Cloudflare location), and 600 creates per client IP (IPv6 by /64). Namespace ids must be unique in your account. A missing block skips that check. - [triggers] crons
- Worker, in wrangler.toml. Defaults to
*/30 * * * *. Reads past expiry are refused either way; the cron frees the storage. - RESEND_API_KEY
- Worker secret, optional. Emails contact-form messages and abuse reports to NOTIFY_EMAIL_TO through Resend.
- NOTIFY_EMAIL_TO
- Worker variable or secret, optional. Where contact-form and abuse-report emails go. Default admin@protectedshare.me.
- NOTIFY_
EMAIL_ DOMAIN - Worker variable or secret, optional. The domain those emails are sent from (deals@ and reports@). Default protectedshare.me. It must be verified in your Resend account.
- ADMIN_TOKEN
- Worker secret, optional, at least 32 characters. Turns on
DELETE /api/admin/:kind/:id(headerX-Admin-Token) to take a share down. Unset or shorter, that route answers 404. - VAPID_PUBLIC_KEY
VAPID_PRIVATE_KEY
VAPID_SUBJECT - Worker secrets, optional. Read notifications. Generate the pair with
npm run vapid:generate -w @protectedshare/api; the subject is amailto:orhttps:address push services can reach you at.
Deploy from GitHub
Optional. The repo ships two workflows.
deploy-api.yml deploys the Worker on pushes to main that touch the API, if the repository has CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID secrets. It doesn't run migrations or create the R2 bucket; do those yourself first.
publish-web-image.yml builds the web image and pushes it to your fork's GitHub Container Registry.
Run it locally
npm install
cd apps/api && npx wrangler d1 execute protectedshare --local --file=schema.sql && cd ../..
echo "API_BACKEND_URL=http://localhost:8787" > apps/web/.env.local
npm run dev:api # API on http://localhost:8787
npm run dev:web # web on http://localhost:3000
npm run dev:mcp # optional: MCP on http://localhost:8790/mcpRun each dev server in its own terminal. Set API_BACKEND_URL as above; without it the web app proxies to the hosted API.