- JavaScript 100%
| docs | ||
| tests | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| worker.js | ||
| wrangler.toml | ||
EdgeStash
A self-contained cloud-drive built as a single Cloudflare Worker, backed by R2 (files) and Workers KV (metadata).
What it is
EdgeStash is a personal/team file storage app that runs entirely on Cloudflare's edge:
- Browser-based file manager with folders, upload, rename, delete, preview
- Password-protected, expiring share links for sending files to people without an account
- Receive links for collecting small files from anonymous visitors into an owner-selected folder
- Admin dashboard for user management and usage stats
- Inline previews for images, PDFs, text/code, video, and audio
- JWT auth with two roles:
admin(single password) anduser(email + password)
The entire deployable application — backend, frontend, router — lives in worker.js. There is no build step.
Repository layout
.
├── worker.js # The whole app: backend + frontend + router (single file)
├── package.json # Local syntax check + regression tests only (no build)
├── tests/ # Node-based regression tests
└── docs/ # Architecture, API, data model, contributing
Deployment
This repository includes a wrangler.toml for the current YCloud deployment. For a new deployment, copy it and replace the bucket/KV IDs, or use the dashboard path below when you are not enabling receive links.
Path A — Cloudflare Dashboard (dashboard-only, no receive links)
If receive links are enabled, use Path B because dashboard-driven setup does not include a migration step equivalent to [[migrations]] for new Durable Object classes (RECEIVE_QUOTA_DO, OBJECT_KEY_RESERVATION_DO).
For deployments without receive links, do:
-
Create the bindings in the Cloudflare Dashboard:
- R2 → Create bucket → e.g.
edgestash-files - Workers & Pages → KV → Create namespace → e.g.
edgestash-kv
- R2 → Create bucket → e.g.
-
Create the Worker:
- Workers & Pages → Create application → Create Worker
- Edit code → delete the scaffold → paste the entire contents of
worker.js - Save and deploy
-
Attach bindings and secrets (Worker → Settings → Variables):
- R2 binding:
R2_BUCKET→edgestash-files - KV binding:
KV_STORE→edgestash-kv - Omit receive-link Durable Object bindings here; use Path B for receive-link-capable deployments.
- Secret (encrypted):
ADMIN_PASSWORD→ a strong password - Secret (recommended):
JWT_SECRET→ a separate strong random string - Variable (for Turnstile receive links, optional):
TURNSTILE_SITE_KEY - Secret (for receive challenge/bootstrap cookies, required for receive links):
RECEIVE_CHAL_HMAC_SECRETS - Secret (for Turnstile receive links, optional):
TURNSTILE_SECRET - Re-deploy after adding bindings — the Worker needs a redeploy to pick them up
- R2 binding:
-
Sign in. Visit
https://<worker-subdomain>.workers.dev/login, switch to the Admin tab, enterADMIN_PASSWORD.
Path B — Wrangler (recommended; required for receive links)
Create wrangler.toml at the repo root:
name = "edgestash"
main = "worker.js"
compatibility_date = "2024-09-01"
[[r2_buckets]]
binding = "R2_BUCKET"
bucket_name = "edgestash-files"
[[kv_namespaces]]
binding = "KV_STORE"
id = "<paste-namespace-id>"
[[durable_objects.bindings]]
name = "RECEIVE_QUOTA_DO"
class_name = "ReceiveQuotaDO"
[[durable_objects.bindings]]
name = "OBJECT_KEY_RESERVATION_DO"
class_name = "ObjectKeyReservationDO"
[[migrations]]
tag = "v1-receive-reservations"
new_sqlite_classes = ["ReceiveQuotaDO", "ObjectKeyReservationDO"]
Then:
# one-time
npm install -g wrangler
wrangler login
wrangler kv:namespace create edgestash-kv
# copy the returned id into wrangler.toml
wrangler secret put ADMIN_PASSWORD
wrangler secret put JWT_SECRET
wrangler secret put RECEIVE_CHAL_HMAC_SECRETS
# optional, only if using Turnstile-required receive links
wrangler secret put TURNSTILE_SECRET
# deploy
wrangler deploy
Local dev:
wrangler dev # local emulation at http://127.0.0.1:8787
wrangler dev --remote # uses real R2/KV
Required bindings
| Name | Type | Required | Purpose |
|---|---|---|---|
R2_BUCKET |
R2 binding | yes | File blob storage |
KV_STORE |
KV binding | yes | Users, shares, stats, rate-limit counters |
RECEIVE_QUOTA_DO |
Durable Object | yes | Receive quota reservation binding |
OBJECT_KEY_RESERVATION_DO |
Durable Object | yes | Final object-key reservation binding |
ADMIN_PASSWORD |
Secret | yes | Admin login password (also JWT fallback) |
JWT_SECRET |
Secret | recommended | Dedicated JWT HMAC key; falls back to ADMIN_PASSWORD if absent |
RECEIVE_CHAL_HMAC_SECRETS |
Secret | receive links | Comma-separated HMAC keys for receive bootstrap/challenge/auth cookies |
TURNSTILE_SITE_KEY |
Variable | optional | Public site key rendered on /r/<linkId> for Turnstile-required links |
TURNSTILE_SECRET |
Secret | optional | Required only for links with Turnstile enabled |
ALLOW_OPEN_RECEIVE_LINKS |
Variable | optional | Set to true only if owners may create receive links without password or Turnstile |
A missing binding causes 500 on the first authenticated request — the Worker does not pre-check bindings at startup.
Receive links are same-origin only; this is not a public cross-origin upload API. Files up to 90 MiB use direct upload; larger receive-link files use R2 multipart upload with in-memory-only fileToken state, so refresh/close cannot resume an in-progress large upload. V1 sends no email notification when files arrive, so owners should check the app regularly for received files and misuse. By default, new receive links must have a password or Turnstile enabled; fully open anonymous links require ALLOW_OPEN_RECEIVE_LINKS=true.
For planned RECEIVE_CHAL_HMAC_SECRETS rotation, deploy new,old, wait at least 10 minutes for active receive challenge/auth cookies to expire, then deploy new. For emergency invalidation, deploy a single fresh key immediately; active Turnstile-required uploaders must re-challenge before auth, upload, or multipart init.
CI deploy (optional)
# .github/workflows/deploy.yml
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
API token scopes: Workers Scripts: Edit, Workers KV Storage: Edit, R2: Edit.
Local checks
npm run check # node --check worker.js (syntax)
npm test # regression tests in tests/
There is no Docker image — Workers run on Cloudflare's runtime, not in containers. wrangler dev is the local equivalent.
Documentation
| File | Audience |
|---|---|
| docs/ARCHITECTURE.md | System design, components, data flow |
| docs/TECH_STACK.md | Technology choices and rationale |
| docs/FEATURES.md | Functional capabilities by domain |
| docs/LOGIC.md | Core algorithms and non-obvious details |
| docs/RUNNING.md | Deployment and environment config (extended) |
| docs/API.md | REST endpoints with request/response shapes |
| docs/DATA_MODEL.md | R2 key conventions, KV schemas, JWT payload |
| docs/CONTRIBUTING.md | Conventions, branching, PR checklist |
Troubleshooting
| Symptom | Likely cause |
|---|---|
401 on every request after login |
ADMIN_PASSWORD (or JWT_SECRET) was rotated → all JWTs invalidated; re-login |
Upload fails with 413 / large body error |
Workers free-plan request body limit (~100 MB) |
500 on /api/files/... |
R2_BUCKET or KV_STORE not attached to the Worker |
Login succeeds but / redirects to /login |
Cookie blocked — check SameSite=Strict isn't stripping it |