Skip to content
Self-host

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.

  1. 1

    Create the database and the bucket

    Create a D1 database, then put the database_id it prints into apps/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.

    Terminal
    cd apps/api
    npx wrangler d1 create protectedshare
    npx wrangler r2 bucket create protectedshare-drops

    Rather keep chunks in D1? Delete the [[r2_buckets]] block from wrangler.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:

    Recommended
    npx wrangler r2 bucket lifecycle add protectedshare-drops expire-old-drops drops/ --expire-days 31
  2. 2

    Apply the schema

    A new database takes the full schema in one go. schema.sql already includes everything from migrations 0001 to 0009.

    New database
    npx wrangler d1 execute protectedshare --remote --file=schema.sql

    Upgrading 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) and 0009_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.

    Existing database, newest migrations
    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.sql
  3. 3

    Deploy the Worker

    wrangler.toml already declares the cleanup cron (*/30 * * * *), Workers observability logs, the R2 binding and six rate limiters. Each limiter's namespace_id (71001 to 71006) must be unique within your Cloudflare account, so change any that clash. Delete a [[ratelimits]] block to skip that check.

    Terminal
    npx wrangler deploy

    If your web app uses its built-in /api proxy, every request reaches the Worker from your server's IP address, so RL_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. Setting NEXT_PUBLIC_API_ORIGIN removes it, because browsers then call the Worker themselves.

    Optional Worker secrets: RESEND_API_KEY emails contact-form messages and abuse reports (without it they're only saved in D1), and ADMIN_TOKEN, at least 32 characters, turns on DELETE /api/admin/:kind/:id for takedowns. The emails go to NOTIFY_EMAIL_TO (default admin@protectedshare.me) from NOTIFY_EMAIL_DOMAIN (default protectedshare.me), which must be a domain your Resend account has verified. Those two aren't secret: set them under [vars] in wrangler.toml, or as secrets.

    Optional
    npx wrangler secret put RESEND_API_KEY
    npx wrangler secret put ADMIN_TOKEN
  4. 4

    Turn 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.

    Terminal
    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.com

    Then build the web app with NEXT_PUBLIC_VAPID_PUBLIC_KEY set 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
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:latest

To build your own image from the repo root (put any NEXT_PUBLIC_* values in apps/web/.env.production first):

Docker
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.

Terminal
cd apps/mcp
npx wrangler deploy

Before 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 (header X-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 a mailto: or https: 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

Terminal
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/mcp

Run each dev server in its own terminal. Set API_BACKEND_URL as above; without it the web app proxies to the hosted API.

Full README on GitHub