Zero-knowledge, end-to-end encrypted notes that disappear after one read.
Try it live · Install the CLI · Documentation · Report a bug
binthere is a zero-knowledge, end-to-end encrypted pastebin. Write a note, get a link, share it — and the note self-destructs the moment it's read. Your browser encrypts everything with AES-256-GCM before it leaves your device, so the server only ever holds ciphertext it can't read. Think of it as a self-destructing envelope for text: secrets, credentials, a private message, a snippet of code.
binthere is a clean-room rebuild inspired by PrivateBin's zero-knowledge model — modern Web Crypto, a strict CSP, atomic burn-after-read, and a real test suite, with the ~700 KB of jQuery/Bootstrap/zlib-WASM stripped out. It runs as a single Cloudflare Worker (Static Assets + KV + a Durable Object), so hosting is cheap and there is no server to maintain.
- No accounts, no tracking. Paste, share, done. There is nothing to sign up for and no analytics watching you do it.
- Nobody can recover a lost link. Not even the operator — there is no key to look up and no index of pastes. The link is the only copy of the key, by design.
- Honest limits. The server still sees IPs, timings, and ciphertext sizes (it's private,
not anonymous), and like all in-browser crypto it trusts the code the site serves — the
full threat model is in
SECURITY.mdand summarized under Limitations.
Pick binthere if you want a paste service you can deploy in one command with nothing to patch, back up, or keep online yourself.
The whole design rests on one trick: where the decryption key lives. It travels in the
URL fragment — the part after # — which browsers never send to any server.
you type ──▶ browser encrypts (AES-256-GCM) ──▶ Worker stores ciphertext only
│ │
key stays in the URL #fragment KV or Durable Object
│ │
recipient opens link ─▶ browser fetches ciphertext ─▶ browser decrypts ─▶ plaintext
- You write a note. Your browser generates a random 256-bit key and encrypts the note locally with AES-256-GCM — before any network request is made.
- Only ciphertext is uploaded. The key is never sent; it's appended to your link after
#. The server stores an opaque blob it has no way to read. - You share the link. It carries both the note's id and the key
(
…/p/<id>#<key>) — the link is the capability to read the note. Optionally, add a password: it's mixed into the key derivation, so neither the link nor the password alone can decrypt. - The recipient opens it. Their browser fetches the ciphertext, reads the key from the fragment, and decrypts locally. The server never sees plaintext at any point.
Every note is one-time view: the first reader atomically consumes it (exactly one winner,
even under simultaneous clicks — a Durable Object guarantees it), and everyone after gets
410 Gone. Unread notes self-delete after 24 hours regardless.
| Feature | Details |
|---|---|
| Zero-knowledge | Encryption and decryption happen only on your device (browser or CLI); the server stores opaque ciphertext and non-secret metadata. |
| Optional password | Layered on top of the URL key — neither alone can decrypt. |
| Burn-after-read | Every note is a strict, atomic single-consumer read (Durable Object). The first reader gets it; everyone else gets 410 Gone. |
| Auto-expiry | Notes delete themselves after 24 hours. |
| Safe rendering | Auto-detected syntax highlighting and a safe Markdown subset (no raw HTML, sanitized links). All rendering is DOM-construction only — never innerHTML. |
| Sharing tools | Copy link, QR code, delete link. |
| Minimal surface | Strict CSP, self-hosted fonts, no third-party scripts, no analytics, no accounts. |
Note
The wire format supports the full expiry range (5 minutes–1 year or never) and non-burn pastes; the current UI simply fixes 24 h + one-time view, so older links keep working.
All of these are solid zero-knowledge paste/secret tools — the difference is mostly in how they are hosted and what they optimize for:
| Project | Server | Storage | Distinguishing traits |
|---|---|---|---|
| binthere | Cloudflare Worker (serverless, no origin server) | Workers KV + Durable Object | Frozen spec with test vectors, atomic burn-after-read, no client framework or build step |
| PrivateBin | PHP | Filesystem / DB / S3 | Mature, many formats, discussions, i18n |
| Yopass | Go | Memcached / Redis | Secret-sharing focus, CLI client |
| cryptgeon | Rust | Redis | File sharing, view limits |
Requires Node.js ≥ 20 (.nvmrc pins 22).
npm install
npm run dev # wrangler dev → http://127.0.0.1:8787KV, the Durable Object, and rate limiting are all emulated locally — no Cloudflare account needed for development.
| Command | Description |
|---|---|
npm run dev |
Local dev server at http://127.0.0.1:8787 |
npm test |
Full Vitest suite: Worker/frontend in the real workerd runtime, then the CLI suite in Node |
npm run test:cli |
Just the CLI suite (cli/, plain Node environment) |
npm run test:watch |
Tests in watch mode |
npm run test:coverage |
Tests with coverage report |
npm run lint |
ESLint 9 (flat config) |
npm run kv:create |
Create the PASTES KV namespace (+ preview) |
npm run deploy |
Deploy to Cloudflare |
CI runs lint, a byte-for-byte test-vector diff, and the full suite.
An official command-line client lives in cli/ and is published to npm as
binthere. It implements the same frozen protocol
as the web client — encryption happens locally, only ciphertext is uploaded, and notes have
the same one read / 24 hours lifecycle as the website. Zero runtime dependencies
(Node ≥ 20 built-ins only).
npm install -g binthere # or try it without installing anything: npx binthereA bare binthere opens an interactive full-screen menu; it also composes in pipelines
(git diff | npx binthere prints a share URL on stdout). See
cli/README.md for the full command reference, interactive-mode tour,
and security notes.
You can run your own binthere — nothing about the design ties it to the public instance, and there is no managed service in the loop. Because it's a single Cloudflare Worker (Static Assets + KV + a Durable Object), it's optimized for Cloudflare Workers and one of its biggest advantages is cost: a complete instance fits inside Cloudflare's free tier, so you can host binthere completely free.
The button above clones the repo and provisions everything declared in
wrangler.toml — the static assets, the PASTES KV binding, the
BurnPaste Durable Object + migration, and the CREATE_RL rate limiter — on your own
Cloudflare account. The importer creates fresh resources and rewrites the resource ids in
your copy of the config; the checked-in ids belong to the origin deployment and are
identifiers, not secrets.
If the one-click path ever misbehaves, the manual route below is the guaranteed fallback:
npm run kv:create # create your own PASTES KV namespace (+ preview)
# paste the printed id / preview_id into wrangler.toml
npm run deploy # creates the Worker, Durable Object, and rate limiterSelf-hosting checklist
- Replace the KV
id/preview_idinwrangler.tomlwith your own (a pristine template is inwrangler.toml.example). - Update the hardcoded canonical URLs:
og:url/og:imageinpublic/index.htmlandCanonicalinpublic/.well-known/security.txtpoint atbinthere.gaury.dev; the footer andsecurity.txtPolicypoint atgithub.com/nxfu/binthere. npm run devworks with placeholder KV ids — KV is emulated locally.- Cost note on
neverexpiry: the official clients always create 24-hour one-time notes, but the wire format (and the API) acceptsexpire: "never". Such a paste is stored with no KV TTL and no Durable Object alarm — an unread burn note kept forever carries a small perpetual cost under SQLite-backed DO storage billing. If you exposeneverto third-party clients, decide whether to cap it or accept the standing cost.
| Piece | Role |
|---|---|
Static Assets (public/) |
SPA frontend, served directly by the Worker |
Worker (src/index.js) |
/api/* paste API — stores ciphertext, enforces size/rate/burn |
KV (PASTES) |
Normal pastes, with native TTL expiry |
Durable Object (BurnPaste) |
Burn-after-read pastes, atomic single-consumer |
| Rate Limiting binding | Abuse mitigation on paste creation (fail-open) |
See ARCHITECTURE.md for the request path and SPEC.md for
the exact cryptographic protocol and paste format v1, including frozen test vectors.
HTTP API
The API only ever handles ciphertext — encryption happens in the client before POST, and
the key fragment never appears in any request. Full details in SPEC.md §10.
| Method & path | Purpose | Success | Errors |
|---|---|---|---|
POST /api/paste |
Create a paste (format v1 JSON) | 201 |
400 invalid · 413 too large · 429 rate-limited |
GET /api/paste/:id |
Fetch a paste (consumes a burn) | 200 |
404 missing/expired · 410 burned |
GET /api/paste/:id?meta=1 |
Peek a burn head without consuming | 200 |
404 missing · 410 burned/expired |
DELETE /api/paste/:id |
Delete, with X-Delete-Token header |
200 |
400 missing token · 403 wrong token · 404 missing |
The delete token travels in a header — never in the URL — so it cannot land in request logs; the server stores and compares only its SHA-256.
Project layout
public/ static frontend (CSP-clean; served by Workers Static Assets)
index.html css/styles.css js/*.js fonts/*.woff2 img/ (favicon.svg + png fallbacks + wordmark[-dark].svg)
_headers robots.txt favicon.ico opengraph.png .well-known/security.txt
src/
index.js Worker: /api/paste routing + asset fallback
burn-do.js BurnPaste Durable Object (atomic burn-after-read)
lib/ ids, storage routing, rate-limit wrapper
test/ vitest suites (run in workerd) + genvectors.mjs (vector regenerator)
+ vectors.expected.txt (pinned vector output, diffed in CI)
tools/ verify-vectors.py — independent Python cross-check of the frozen vectors
cli/ the npm-published CLI client (own package.json + Node-environment tests;
vendor/ mirrors public/js/{bytes,format,crypto,qrcode}.js, drift-tested)
SPEC.md SECURITY.md ARCHITECTURE.md
CHANGELOG.md CONTRIBUTING.md CODE_OF_CONDUCT.md LICENSE
public/js/{bytes,crypto,format,markdown}.js are shared: the browser imports them as static
assets and the Worker bundles the same files, so the paste format has a single source of truth.
Most of these are deliberate scope choices, not bugs. Know them before relying on binthere:
- Not anonymous or metadata-free. The server sees IP, timing, ciphertext size, and the
non-secret
adata(IVs, KDF params, format flags). It only cannot read your plaintext (SECURITY.md§3). - No protection from a compromised deployment. Decryption runs in JavaScript the server
delivers, so a malicious or hacked host could serve code that leaks your key. In-browser E2E
encryption trusts the origin (
SECURITY.md§4). - Lose the link, lose the note. No accounts, no server-side index — the id + key exist only in the URL you share. Nobody, including you, can recover or list pastes.
More limitations
- Burn passwords can be brute-forced offline. The non-consuming peek returns the wrapped
key so a password can be checked before the single read — someone who already has the URL
secret can guess a weak password without burning the note. Use a strong password
(
SPEC.md§8 documents the trade-off). - Password KDF is PBKDF2-SHA256 (310k iterations), not a memory-hard KDF. Argon2id is on the roadmap.
- The UI fixes expiry at 24 h and one-time view. The wire format supports more; the controls are just hidden.
- English only.
- The rate limiter fails open — it is abuse mitigation, not access control.
- Canonical URLs are hardcoded to the origin deployment; update them when self-hosting (see Self-hosting).
Can the operator read my notes?
No. Content is encrypted with AES-256-GCM in your browser before upload; the server stores
only ciphertext and non-secret metadata. The decryption key lives in the URL fragment, which
browsers never send to the server. What the server does see (IP, timing, sizes) is spelled
out in SECURITY.md §3.
I lost the link — can the note be recovered?
No. There are no accounts and no server-side index; the paste id and decryption key exist only in the URL. Without it, the ciphertext is unrecoverable — by design.
Why does my link say "expired or was already opened"?
Every note is one-time view: the first reader atomically consumes it, and everyone after
(including you, if you open your own link first) gets 410 Gone. Notes also self-delete
after 24 hours even if never opened.
Does adding a password make the link safe to send in the clear?
It helps — the password is mixed into the key derivation, so the link alone cannot decrypt.
But someone holding the link can test passwords offline without burning the note, so a weak
password only slows them down. Use a strong password and send it over a different channel
(SPEC.md §8).
What does the recipient need?
Just the link and any modern browser — Web Crypto (SubtleCrypto) is the only requirement.
No account, extension, or app.
Can I create pastes from a script or CLI?
Yes — the official CLI is published to npm: npm install -g binthere (or npx binthere).
It speaks the same frozen protocol as the web client and is tested against the same vectors;
see the CLI section. Third-party clients are possible too: the HTTP API accepts only
ciphertext in paste format v1, and the frozen test vectors in SPEC.md make an
independent implementation verifiable.
Roughly in priority order:
- Official CLI client — shipped; on npm as
binthere(see CLI) - Argon2id as a versioned password-KDF option alongside PBKDF2 (spec-first: vectors before code)
- File attachments — encrypted binary blobs with size limits (likely R2 for large files)
- Headless-browser CSP + render test (Playwright) in CI, asserting zero CSP violations across the create/view/burn flows
binthere is a security-sensitive cryptographic application. The threat model, explicit
non-goals, and vulnerability-reporting process are in SECURITY.md; the
frozen protocol and paste format live in SPEC.md.
Important
Report suspected vulnerabilities privately to nxfu@proton.me
(see SECURITY.md §8). Do not open a public issue with exploit details.
Issues and PRs are welcome — see CONTRIBUTING.md. Two hard rules:
- Crypto is spec-first. Any change to the protocol, paste format, or canonical AAD must
update
SPEC.mdfirst — never silently — then regenerate the frozen vectors withnode test/genvectors.mjs(and refreshtest/vectors.expected.txt, which CI diffs byte-for-byte). Never hand-edit the pinned hexes intest/crypto.test.js. - Keep the CSP strict and rendering XSS-safe. No inline styles/scripts, no CDNs, no
innerHTMLon user content. New rendering paths need a case intest/markdown.test.js.
Run npm run lint and npm test before opening a PR; all suites run in the real workerd
runtime.
- Cloudflare Workers — Static Assets, KV, Durable Objects, native rate limiting
- Vanilla JavaScript (native ES modules) — no framework, no bundler for the frontend
- Web Crypto API — AES-256-GCM, PBKDF2-SHA256
- Vitest — Worker suites in the real
workerdruntime, CLI suites in Node - ESLint 9 — flat config
- PrivateBin — the zero-knowledge pastebin whose model this is a clean-room rebuild of
- qrcode-generator by Kazuhiko Arase
(MIT) — vendored in
public/js/qrcode.jsfor offline, CSP-safe QR rendering - Newsreader by Production Type,
Geist by Vercel, and
JetBrains Mono by JetBrains (all SIL OFL 1.1) —
self-hosted in
public/fonts/; license texts inpublic/fonts/THIRD-PARTY-NOTICES.md
MIT © 2026 nxfu
Built to be shared once and forgotten.
If binthere is useful to you, consider giving it a ⭐ — it helps others find it.