Cloudsteading
Binthere terminal menu for creating, viewing and deleting encrypted notes

binthere

Encrypted text pastes that expire after one read, served by one Cloudflare Worker.

binthere is a self-hosted PrivateBin alternative built on Cloudflare (Durable Objects, KV, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: MIT

License TL;DR

You can use it, change it, self-host it and sell it. Keep the original copyright and license notice with copies of the code. You don’t have to publish your changes. The authors don’t promise it will work.

Explain MIT in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@nxfu

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

The reviewed Cloudflare deployment is eligible for Free-plan allowances for the stated small workload and feature scope. Usage limits, CPU, required account setup and separate services apply.

Hosting requirements
  • Use the declared new_sqlite_classes migration; Free cannot host legacy KV-backed Durable Objects.
  • Official 24-hour clients bound retention; API clients allowing never expiry create persistent storage and require a separate budget.
  • Keep KV writes, deletes and lists within daily quotas, and measure Worker CPU and Durable Object requests/duration under actual traffic.
  • Workers Free dynamic requests are shared across this account (100,000/day), with 10 ms CPU per invocation; workload fit is conditional and has not been measured.
  • KV Free allowance: 100,000 keys read/day and 1,000 each writes/deletes/list requests/day; this may constrain updates before Worker request limits.
  • Only SQLite Durable Objects qualify for Workers Free. Keep DO requests below 100,000/day, active duration below 13,000 GB-s/day and SQLite storage/operations inside the captured allowances.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: 63e5544. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • privatebin ↗

    ttps://ziadoua.github.io/m3-Markdown-Badges/badges/ESLint/eslint3.svg"></a> </p> ## Why binthere? binthere is a clean-room rebuild inspired by [PrivateBin](https://privatebin.info)'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 a

  • workers ↗

    d / SECURITY.md / ARCHITECTURE.md. # Worker script name is "binthere" (→ deploy URL binthere.<subdomain>.workers.dev); # the KV namespace keeps the "PASTES" name. name = "binthere" main = "src/index.js" compatibility_date = "2025-10-11" # ── Static assets ──────────────────────────────────────────────────────────── # The frontend lives in public/. The Worker runs first only for /api/*; every # other path is served from static assets, with a single-page-application # fallback so /p/<id> serves index.html (the client reads the id + fragment key). [assets] director

  • kv ↗

    t name is "binthere" (→ deploy URL binthere.<subdomain>.workers.dev); # the KV namespace keeps the "PASTES" name. name = "binthere" main = "src/index.js" compatibility_date = "2025-10-11" # ── Static assets ──────────────────────────────────────────────────────────── # The frontend lives in public/. The Worker runs first only for /api/*; every # other path is served from static assets, with a single-page-application # fallback so /p/<id> serves index.html (the client reads the id + fragment key). [assets] directory = "./public" binding = "ASSETS" not_found_handli

  • durable-objects ↗

    ct: burn-after-read pastes (atomic single-consumer) ────────── [[durable_objects.bindings]] name = "BURN" class_name = "BurnPaste" [[migrations]] tag = "v1" new_sqlite_classes = [ "BurnPaste" ] # ── Native rate limiting: paste creation, keyed by client IP ───────────────── # Abuse mitigation, not authentication; the code fails open if unavailable. [[ratelimits]] name = "CREATE_RL" namespace_id = "1001" [ratelimits.simple] limit = 30 period = 60 # ── Observability ──────────────────────────────────────────────────────────── [observability] enabled = true

  • free-tier-eligible ↗

    ting. # One deploy unit, one config — see SPEC.md / SECURITY.md / ARCHITECTURE.md. # Worker script name is "binthere" (→ deploy URL binthere.<subdomain>.workers.dev); # the KV namespace keeps the "PASTES" name. name = "binthere" main = "src/index.js" compatibility_date = "2025-10-11" # ── Static assets ──────────────────────────────────────────────────────────── # The frontend lives in public/. The Worker runs first only for /api/*; every # other path is served from static assets, with a single-page-application # fallback so /p/<id> serves index.html (the client

  • free-tier-eligible ↗

    ount Manager. | | Requests<sup>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 second

  • free-tier-eligible ↗

    flare.com/workers/platform/pricing/). | | Free plan<sup>1</sup> | Paid plan | | --- | --- | --- | | Keys read | 100,000 / day | 10 million/month, + $0.50/million | | Keys written | 1,000 / day | 1 million/month, + $5.00/million | | Keys deleted | 1,000 / day | 1 million/month, + $5.00/million | | List requests | 1,000 / day | 1 million/month, + $5.00/million | | Stored data | 1 GB | 1 GB, + $0.50/ GB-month | <sup>1</sup> The Workers Free plan includes limited Workers KV usage. All limits reset daily at 00:00 UTC. If you exceed any one of these limits, further o

  • free-tier-eligible ↗

    ute and storage. Note Durable Objects are available both on Workers Free and Workers Paid plans. - **Workers Free plan**: Only Durable Objects with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) are available. - **Workers Paid plan**: Durable Objects with the SQLite storage backend are available. The [key-value storage backend](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/#storage-backends) is only avail

  • MIT ↗

    MIT License Copyright (c) 2026 nxfu Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this

  • architecture ↗

    d / SECURITY.md / ARCHITECTURE.md. # Worker script name is "binthere" (→ deploy URL binthere.<subdomain>.workers.dev); # the KV namespace keeps the "PASTES" name. name = "binthere" main = "src/index.js" compatibility_date = "2025-10-11" # ── Static assets ──────────────────────────────────────────────────────────── # The frontend lives in public/. The Worker runs first only for /api/*; every # other path is served from static assets, with a single-page-application # fallback so /p/<id> serves index.html (the client reads the id + fragment key). [assets] director

Upstream screenshot · nxfu/binthere repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

PrivateBin logoPrivateBin ↗

Editorial workflow alternative: Client-side encrypted text sharing with expiry and burn-after-read; file attachments, discussion threads and language parity not asserted.

See supporting source ↗
external SaaS target
varies

How it works

The shape of binthere on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
binthere
wrangler.toml
↓
App
binthere
entry
Cloudflare Workers
Entrypoint: src/index.js
↓

Configuration and workflow sources

Reviewed commit 63e5544d38d3. Files were read as data; upstream applications and CI jobs were not executed.

Deployment configuration · 1 files
wrangler.toml ↗

Cloudflare Workers · compatibility 2025-10-11

binthere · default

Entrypoint: src/index.js

Static assets: ./public · single-page-application · Worker first: ["/api/*"]

  • PASTES → KV
  • BURN → Durable Objects · class BurnPaste
  • ASSETS → Static assets

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/index.js ↗
  • L30 · fetch handler exported · references ASSETS · calls pathname.startsWith, env.ASSETS.fetch, createPaste, err, readStars, pathname.match, decodeURIComponent, consumePaste, readPaste, url.searchParams.get, deletePaste
  • L87 · readCappedBody calls (conditional paths may differ): stream.getReader, reader.read, reader.cancel, chunks.push, reader.releaseLock, out.set
  • L117 · createPaste calls (conditional paths may differ): toLowerCase, trim, split, request.headers.get, err, allowCreate, Number, Number.isFinite, readCappedBody, JSON.parse, decode, validatePaste, ttlSeconds, genDeleteToken, hashToken, Math.floor, Date.now, JSON.stringify, genId, create
  • L203 · readPaste calls (conditional paths may differ): parseId, err, peek, burnStub, json, kvGet
  • L234 · consumePaste calls (conditional paths may differ): toLowerCase, trim, request.headers.get, err, parseId, consume, burnStub, json
  • L256 · readStars calls (conditional paths may differ): fetchStars, err, json
  • L264 · deletePaste calls (conditional paths may differ): request.headers.get, err, parseId, remove, burnStub, json, kvGet, verifyToken, kvDelete

Environment references: env.ASSETS

public/js/format.js ↗
  • L38 · isPlainObject calls (conditional paths may differ): Array.isArray
  • L43 · assertNoDangerousKeys calls (conditional paths may differ): Object.prototype.hasOwnProperty.call
  • L52 · assertExactKeys calls (conditional paths may differ): assertNoDangerousKeys, Object.keys, allowed.includes, Object.prototype.hasOwnProperty.call
  • L65 · b64urlByteLength calls (conditional paths may differ): bytesFromB64url
  • L78 · buildAAD calls (conditional paths may differ): encode, lines.join
  • L94 · validateWk calls (conditional paths may differ): b64urlByteLength
  • L102 · validateAdata calls (conditional paths may differ): isPlainObject, assertExactKeys, KDFS.includes, COMP.includes, FORMATS.includes, Number.isInteger, b64urlByteLength
  • L139 · validateMeta calls (conditional paths may differ): isPlainObject, assertNoDangerousKeys, Object.keys, EXPIRE_OPTIONS.includes, Object.prototype.hasOwnProperty.call, Number.isInteger
  • L159 · validatePaste calls (conditional paths may differ): isPlainObject, assertExactKeys, b64urlByteLength, validateWk, validateAdata, validateMeta
  • L183 · validateHead calls (conditional paths may differ): isPlainObject, assertExactKeys, validateWk, validateAdata, validateMeta
src/lib/ids.js ↗
  • L17 · genId calls (conditional paths may differ): b64urlFromBytes, randomBytes
  • L26 · parseId calls (conditional paths may differ): id.slice, bytesFromB64url
  • L40 · genDeleteToken calls (conditional paths may differ): b64urlFromBytes, randomBytes
  • L45 · hashToken calls (conditional paths may differ): sha256Hex, utf8
  • L54 · verifyToken calls (conditional paths may differ): bytesFromB64url, hashToken, timingSafeEqualHex
src/lib/store.js ↗
  • L26 · kvExists calls (conditional paths may differ): env.PASTES.get
  • L30 · kvPut calls (conditional paths may differ): env.PASTES.put, JSON.stringify
  • L36 · kvGet calls (conditional paths may differ): env.PASTES.get, JSON.parse
  • L46 · kvDelete calls (conditional paths may differ): env.PASTES.delete
  • L53 · burnStub calls (conditional paths may differ): env.BURN.get, env.BURN.idFromName

Environment references: env.PASTES · env.BURN

src/lib/ratelimit.js ↗
  • L7 · allowCreate calls (conditional paths may differ): request.headers.get, rl.limit

Environment references: env.CREATE_RL

src/lib/stars.js ↗
  • L21 · fetchStars calls (conditional paths may differ): fetch, res.json, parseStars
  • L50 · parseStars calls (conditional paths may differ): Array.isArray, Number.isInteger
public/js/bytes.js ↗
  • L10 · utf8 calls (conditional paths may differ): _enc.encode
  • L15 · fromUtf8 calls (conditional paths may differ): _dec.decode
  • L20 · randomBytes calls (conditional paths may differ): crypto.getRandomValues
  • L27 · hex calls (conditional paths may differ): padStart, toString
  • L39 · b64urlFromBytes calls (conditional paths may differ): String.fromCharCode.apply, bytes.subarray, replace, btoa
  • L54 · bytesFromB64url calls (conditional paths may differ): _B64URL_RE.test, _B64URL_ALPHABET.indexOf, replace, str.replace, slice, atob, bin.charCodeAt
  • L82 · sha256 calls (conditional paths may differ): crypto.subtle.digest
  • L88 · sha256Hex calls (conditional paths may differ): hex, sha256
  • L99 · timingSafeEqualHex calls (conditional paths may differ): a.charCodeAt, b.charCodeAt
Build and deployment pipeline · 2 GitHub Actions workflows

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

CI · .github/workflows/ci.yml ↗

Triggers: push, pull_request

test · no job dependencies declared

  1. actions/checkout@v4actions/checkout@v4
  2. actions/setup-node@v4actions/setup-node@v4
  3. Shell commandnpm ci
  4. Shell commandnpm run lint
  5. Shell commandnode test/genvectors.mjs | diff -u test/vectors.expected.txt -
  6. Shell commandnpm run test:coverage
  7. Shell commandnpm run test:dom
  8. Shell commandnpm run test:cli

test-cli · no job dependencies declared

  1. actions/checkout@v4actions/checkout@v4
  2. actions/setup-node@v4actions/setup-node@v4
  3. Shell commandnpm ci
  4. Shell commandnpm run test:cli
Release CLI · .github/workflows/release.yml ↗

Triggers: push

publish · no job dependencies declared

  1. actions/checkout@v4actions/checkout@v4
  2. actions/setup-node@v4actions/setup-node@v4
  3. Shell commandnpm ci
  4. Shell commandnpm run lint
  5. Shell commandnode test/genvectors.mjs | diff -u test/vectors.expected.txt -
  6. Shell commandnpm test
  7. Verify tag matches cli/package.json versiontag="${GITHUB_REF_NAME#cli-v}" pkg="$(node -p "require('./cli/package.json').version")" if [ "$tag" != "$pkg" ]; then echo "tag cli-v$tag does not match cli/package.json version $pkg" >&2 exit 1 fi
  8. Shell commandnpm publish --provenance --access public
package.json ↗
  • deploy: wrangler deploy

Full upstream document by @nxfu · README.md · snapshot 63e5544

binthere wordmark

Say it once. Sealed.

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.

Node.js JavaScript HTML CSS ESLint

Why binthere?

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.md and 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.

How it works

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.

flowchart TD
    A([You write a note]) --> B[Your browser locks it<br/>before it leaves your device]
    B -->|locked note only| C[(Server stores it<br/>for up to 24 hours)]
    B -->|the secret key stays here| D[Share link]
    C --> E[Recipient opens the link]
    D --> E
    E --> F([Their browser unlocks the note<br/>and the server deletes its copy])
  1. 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.
  2. 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.
  3. 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.
  4. 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.

Features

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.

How it compares

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

Getting started

Requires Node.js ≥ 20 (.nvmrc pins 22).

npm install
npm run dev      # wrangler dev → http://127.0.0.1:8787

KV, 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.

CLI

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 binthere

A 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. Set BINTHERE_NO_ANIMATION=1 to keep the colored TUI while disabling non-essential motion. Globally installed copies can update themselves with binthere update.

Self-hosting

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.

Deploy to Cloudflare

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 limiter
Self-hosting checklist
  • Replace the KV id / preview_id in wrangler.toml with your own (a pristine template is in wrangler.toml.example).
  • Update the hardcoded canonical URLs: og:url / og:image in public/index.html and Canonical in public/.well-known/security.txt point at binthere.gaury.dev; the footer and security.txt Policy point at github.com/nxfu/binthere.
  • The link-preview card public/opengraph.png also has that domain printed on it. Edit tools/opengraph.html and re-render with node tools/render-og.mjs (needs a local Chrome/Chromium; pass --browser <path> or set $CHROME if it isn't found).
  • npm run dev works with placeholder KV ids — KV is emulated locally.
  • Cost note on never expiry: the official clients always create 24-hour one-time notes, but the wire format (and the API) accepts expire: "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 expose never to third-party clients, decide whether to cap it or accept the standing cost.

Architecture

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
GET /api/stars Repo star count for the topbar badge (not part of the paste protocol) 200 502 GitHub unavailable

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, GitHub star proxy
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
                   opengraph.html + render-og.mjs — source & renderer for public/opengraph.png
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.

Limitations

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

FAQ

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.

Roadmap

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

Security

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.

Contributing

Issues and PRs are welcome — see CONTRIBUTING.md. Two hard rules:

  1. Crypto is spec-first. Any change to the protocol, paste format, or canonical AAD must update SPEC.md first — never silently — then regenerate the frozen vectors with node test/genvectors.mjs (and refresh test/vectors.expected.txt, which CI diffs byte-for-byte). Never hand-edit the pinned hexes in test/crypto.test.js.
  2. Keep the CSP strict and rendering XSS-safe. No inline styles/scripts, no CDNs, no innerHTML on user content. New rendering paths need a case in test/markdown.test.js.

Run npm run lint and npm test before opening a PR; all suites run in the real workerd runtime.

Tech stack

  • 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 workerd runtime, CLI suites in Node
  • ESLint 9 — flat config

Acknowledgements

License

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.

Frequently asked about binthere

What is binthere?+

binthere is a self-hosted PrivateBin alternative built on the Cloudflare developer platform. Encrypted text pastes that expire after one read, served by one Cloudflare Worker.

What does binthere replace?+

binthere is listed as an alternative to PrivateBin. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does binthere use?+

binthere is built on Durable Objects, KV, Workers.

How much does binthere cost to run?+

The reviewed Cloudflare deployment is eligible for Free-plan allowances for the stated small workload and feature scope. Usage limits, CPU, required account setup and separate services apply. Use the declared new_sqlite_classes migration; Free cannot host legacy KV-backed Durable Objects. Official 24-hour clients bound retention; API clients allowing never expiry create persistent storage and require a separate budget. Keep KV writes, deletes and lists within daily quotas, and measure Worker CPU and Durable Object requests/duration under actual traffic. Workers Free dynamic requests are shared across this account (100,000/day), with 10 ms CPU per invocation; workload fit is conditional and has not been measured. KV Free allowance: 100,000 keys read/day and 1,000 each writes/deletes/list requests/day; this may constrain updates before Worker request limits. Only SQLite Durable Objects qualify for Workers Free. Keep DO requests below 100,000/day, active duration below 13,000 GB-s/day and SQLite storage/operations inside the captured allowances. Check current Cloudflare pricing before deploying.

Is binthere open source?+

The upstream repository declares the MIT license. Read its terms at https://raw.githubusercontent.com/nxfu/binthere/63e5544d38d3ffc439978a3bf62d287ce57fe83f/LICENSE. Source code and contributor credit are available at https://github.com/nxfu/binthere.

Discussion · 0

sign in to comment →
No comments yet — be the first.