Secure, expiring secret sharing: you write content (Markdown or rich text), optionally attach a file and protect it with a passphrase, then share a link. The server never sees plaintext. Encryption runs in the browser; the Cloudflare Worker stores only ciphertext, verification blobs, hashed keys, and TTL metadata.
- Client-side encryption — Text-only secrets use
v3.j.(PBKDF2 + AES-GCM). Secrets with a file attachment usev4.j./v4.f.: one PBKDF2 pass, then HKDF-derived subkeys for the test string, body JSON, and file. Older links may still usev3+v3(two PBKDF2) or legacy CryptoJS (decrypt-only). - Password-protected secrets — Offline guessing cost is dominated by PBKDF2 iterations; users should pick strong passphrases (the UI shows rough strength hints).
- Optional burn-after-read — Supported for text-only flows; attachments use a separate burn-token window where configured.
- Manage link — After creating a secret, owners get a manage URL (stored briefly in
localStorage) to copy the viewer link or delete the secret server-side. - i18n — English and Russian (
apps/frontend/src/locales).
Browser (React/Vite)
│ HTTPS
▼
Cloudflare Worker (apps/backend)
├── Static assets ─ apps/frontend/dist (SPA)
└── /api/* → HTTP handler → Durable Objects
├── SECRETS → SQLite-backed SecretObject (+ R2 attachment refs)
└── METRICS → MetricsObject (counters)
Shared logic (packages/shared) includes:
- Cryptographic primitives and legacy compatibility
- Zod request validation
- API route paths, URL builders, and stable error strings
- Types for requests/responses and stored rows
apps/frontend React SPA (Vite, Tailwind, shadcn-style UI)
apps/backend Worker entrypoint + Durable Objects + api/handler.ts
packages/shared Types, crypto, validation, routes, api-errors
tests Bun unit tests
wrangler.jsonc Workers config (assets, DOs, R2, env vars)
- Bun (package manager and test runner)
- Wrangler (installed via project devDependencies; use
bunx wranglerornpx wranglerif needed)
Install dependencies:
bun installRun the full stack locally (builds the frontend, then starts the Worker with the development env from wrangler.jsonc):
bun run devUseful scripts:
| Script | Purpose |
|---|---|
bun run build |
Production build of the frontend + TypeScript check all packages |
bun run build:frontend |
Vite build only (apps/frontend/dist) |
bun run typecheck |
tsc --noEmit for shared, backend, frontend |
bun run test |
Bun tests in tests/ |
bun run lint |
Biome check |
bun run format |
Biome format write |
bun run cf:typegen |
Regenerate apps/backend/src/worker-configuration.d.ts after Wrangler binding changes |
Preview with local persistence behavior:
bun run previewAll JSON routes live under /api. The frontend uses helpers from packages/shared (apiUrl, etc.) so paths stay consistent with apps/backend/src/api/handler.ts.
| Method | Path | Notes |
|---|---|---|
| GET | /api/health |
Liveness + version info |
| POST | /api/secrets |
Create secret; body includes ciphertext, testCiphertext, hashed keys |
| GET | /api/secrets/:accessKeyHash |
Fetch ciphertext bundle (64-char hex id) |
| DELETE | /api/secrets/:accessKeyHash/:manageKeyHash |
Owner delete |
| PUT | /api/secrets/:accessKeyHash/attachment |
Upload encrypted attachment (X-Upload-Token) |
| GET | /api/secrets/:accessKeyHash/attachment |
Download (optional burnToken query) |
| GET | /api/metrics |
Disabled until METRICS_TOKEN is set; then requires Authorization: Bearer … |
| POST | /api/metrics |
Same auth as GET; body JSON sets absolute counter values (merge). Example: {"created":1000,"requested":5000}. Omitted keys are left unchanged. |
Plaintext never appears in these payloads.
The Worker uses Cloudflare’s Rate Limiting binding before the HTTP API handler. Limits are applied per client IP (CF-Connecting-IP, then X-Forwarded-For, else a shared unknown bucket in dev).
Default policy (see ratelimits in wrangler.jsonc): 120 requests per 60 seconds per IP per namespace. Excess traffic receives 429 with JSON { "error": "Too many requests" } and a Retry-After header (60 seconds). Static assets are not rate limited. If wrangler deploy errors on the ratelimit binding, confirm your Cloudflare account supports Workers Rate Limiting.
Production and development env each declare their own ratelimits block (Wrangler does not inherit this section). Adjust simple.limit / simple.period (allowed periods are 10 or 60 seconds) and keep API_RATE_LIMIT_PERIOD_SEC in apps/backend/src/rate-limit.ts aligned with period for Retry-After.
wrangler.jsonc defines:
- Worker entry:
apps/backend/src/index.ts - Static assets directory:
apps/frontend/dist - R2 bucket binding for attachments
- Durable Object namespaces
SECRETSandMETRICS ratelimitsbindingAPI_RATE_LIMITERfor/api/*(see above)- Non-secret
vars(APP_URL,ENVIRONMENT,VERSION_PREFIX, …)
# Optional: metrics endpoint bearer token
wrangler secret put METRICS_TOKEN
# Optional: authenticated migration/import endpoint
wrangler secret put MIGRATION_TOKENFor local development you can put the same keys in a .dev.vars file (not committed).
Seeding historical metrics (e.g. after moving from a previous stack), with METRICS_TOKEN in place:
curl -sS -X POST "https://secred.link/api/metrics" \
-H "Authorization: Bearer $METRICS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"created":12345,"requested":67890}'Use non-negative integers; allowed keys: created, requested, burned, deleted, expired, migration_imported. Then GET /api/metrics to verify.
bun run build
wrangler deploy(bun run deploy runs a frontend build then wrangler deploy with the default environment.)
- Viewer links carry the raw access key in the URL fragment; anyone with the link can request ciphertext from the API. Passphrase-protected secrets depend on password strength for confidentiality.
- The Worker stores double-hashed access/manage identifiers; it cannot derive raw keys from stored rows.
- Additional hardening: dashboard WAF / bot rules on your hostname, dependency updates, and CSP in
apps/frontend/index.html.
Repository-specific hints for tools and agents live in AGENTS.md (layout, commands, conventions).
Private project ("private": true in package.json). Adjust this section if you open-source the repo.