Running PrivateBin means running a PHP server, permanently. Not just on install day: the web server, the database, TLS termination, and someone who keeps all three patched for as long as the instance lives. Recurring PHP runtime security news is a reminder of that standing cost. It is not a reason to panic, and it is not an argument against PrivateBin itself, which is a good, well-tested project. It is simply the maintenance shape of the self-hosted PHP model: a server you own, forever.
Binflare is an attempt to keep the PrivateBin protocol and drop that shape. It is a zero-knowledge pastebin that speaks the PrivateBin 2.x protocol, rewritten to run entirely on Cloudflare Workers, D1, and R2. There is no server to maintain, no PHP runtime to patch, and no container image to rebuild. The project is open source under MIT at https://github.com/openshiye/binflare, and a live preview runs at https://binflare.199595.xyz.
What Binflare is
The idea is deliberately narrow. Binflare does not invent a new paste format, a new client, or a new sharing model. It reimplements the PrivateBin 2.1.0 wire format and JSON API on a different runtime. Pastes are encrypted in the browser before upload. The server stores ciphertext plus the minimum metadata needed for expiry, burn-after-reading, comments, and deletion. The decryption key lives only in the URL fragment and is never sent to the server.
Because the wire format is the same, an existing PrivateBin client can point its base URL at a Binflare deployment and keep working: same paste format, same fragment keys, same expiry options, same comments, same delete links. That compatibility is the whole point. If it did not hold, Binflare would just be another pastebin.
One boundary matters and is stated up front: protocol compatibility does not mean data migration. Binflare does not import pastes from an existing instance. Pastes created on another instance stay there.
The protocol compatibility work
The protocol baseline is pinned to the PrivateBin 2.1.0 release, reference checkout 2.1.0-83-ga88aa802, and locked down by fixtures in tests/compat/baseline.json. An upgrade of that baseline is always a deliberate, separate change, verified against the fixtures, never a floating dependency bump.
Concretely, what matches:
- The v2 payload shape:
adata,ct, andmeta, with commentpasteidandparentidfields in the same wire shape. - Encryption: new pastes use AES-GCM, with keys derived from a 256-bit random fragment key plus an optional password via PBKDF2, and raw DEFLATE compression. Legacy
ctrandcbcpastes are read-only: they lack authentication, are shown with a warning, and are never created by Binflare. - Paste IDs: the protocol's FNV-1a 64-bit checksum of the ciphertext, rendered as 16 hex characters.
- Delete tokens: HMAC-SHA256 over the paste ID with a per-paste 256-byte salt, where the salt is never returned in read responses.
- URLs and the root API contract:
?id#key,?pasteid=, theX-Requested-Withheader convention, andstatus: 0/1responses.
The format details are where a rewrite like this usually drifts, so they are documented rather than assumed. Where Binflare deliberately diverges, docs/PARITY.md lists it in the open. The divergences are few and specific: JSON-LD endpoints are not implemented (?jsonld= returns the error response, not metadata); comment avatars are generated client-side from the comment ID instead of from IP-derived data; the UI ships in English and Simplified Chinese rather than upstream's 39 locales; and storage backends are D1 plus R2 instead of files, MySQL, or object stores. Shortener proxies, template switching, and an admin CLI are also out of scope, because they belong to the PHP deployment model, not to the protocol.
There are also upstream quirks that Binflare deliberately does not copy. Upstream burn-after-reading reads and then deletes, which leaves a race; upstream delete verifies existence by reading before checking the token, which can destroy a burn paste with a wrong token; and upstream comment parents are not checked. Binflare checks the token before deleting, validates that a comment parent belongs to the same paste, and handles burn as a single atomic operation, described below. These are implementation improvements, not changes to the wire format, and they are recorded in the same parity document.
Storage: D1 for small ciphertext, R2 for large
Binflare stores metadata and small ciphertext in D1, Cloudflare's SQLite database. D1 has a 2,000,000-byte row and value limit, so Binflare caps D1 ciphertext at 1.875 MB. Ciphertext above that spills to R2, with a default cap of 10 MB. The API still returns a single PrivateBin v2 ct either way, so clients never see the split.
Burn-after-reading is where the storage choice pays off. Upstream's read-then-delete has a race under concurrent reads. Binflare performs burn in one D1 batch transaction: select the paste and delete it, together with its comments, atomically. Exactly one concurrent reader can receive the payload. That property is covered by a concurrency test in the repository: 20 parallel reads, exactly one success.
Expiry is handled the same way upstream handles it, adapted to the platform: lazy checks on read, plus a scheduled Cron cleanup job that physically deletes expired pastes. Expired pastes return the generic "does not exist" error on read; physical deletion happens lazily and via Cron, since D1 has no native TTL.
There is one honest wart in the R2 path. Burn, delete, and expiry remove the D1 row first and then delete the R2 object on a best-effort basis. If that second delete fails, the D1 row is gone but ciphertext may remain orphaned in the bucket. The deployment docs therefore ask operators to configure an R2 lifecycle rule (the documented example expires objects under the pastes/ prefix after 366 days) or run their own cleanup for strict storage hygiene. The lifecycle rule bounds how long orphaned ciphertext can remain; it does not make burn atomic across D1 and R2.
The deployment lesson: a Workers-only token cannot run migrations
Binflare deploys with a one-click button in the README. The button provisions D1 and R2 and deploys the Worker. What I learned while wiring that up: a Workers-only deploy token cannot run D1 migrations. The attempt fails with error 7403, because the token can deploy Worker bindings but cannot query D1 directly.
The fix is structural rather than clever. The Worker now initializes its D1 schema idempotently on its first API request and registers the migrations as applied. A fresh account therefore goes from clicking the button to a working pastebin without touching a console or running a migration by hand. The deploy command on the button page must stay as npm run deploy, not the default npx wrangler deploy, because the committed wrangler.jsonc carries a placeholder D1 ID and the project's deploy script injects the provisioned IDs after build.
Two deployment notes from the same section are worth stating plainly. Fresh Cloudflare accounts may need to enable R2 first (a payment method is required, even within the free allowance) and redeploy once; the app detects a missing R2 binding, starts in the D1-only tier, and explains what to do at /setup.
The free-tier arithmetic
The reason this deployment shape works for a personal or small-team instance is the free tier, stated with its actual numbers:
- Workers free plan: 100,000 requests per day.
- D1 free plan: 5,000,000 rows read per day, 100,000 rows written per day, 500 MB per database.
- R2 free tier: 10 GB of storage.
There is no VPS bill, no database server, and no runtime to patch. The preview instance at binflare.199595.xyz runs the small tier on purpose: D1 only (R2 disabled), rate-limited, with expiry capped at one week and a default of one day. Deploying your own instance is what unlocks the full limits, on your own Cloudflare account and your own free tier.
The arithmetic also has a stated limit, and it concerns CPU rather than requests. The Workers free plan allows 10 ms of CPU per request. Local measurements in the README put the Workers runtime FNV computation at roughly 25–27 ms of CPU for a maximum-size 2.5 MB base64 string, so maximum-size D1 requests can exceed the free plan's CPU allowance, and the 10 MB R2 path is not free-plan CPU-safe either. For heavy large-paste usage, the documented answer is Workers Paid or lower caps. The free tier covers ordinary personal use; it does not cover unlimited maximum-size pastes.
Deliberate limitations
Two omissions are intentional. First, there are no user accounts. Like PrivateBin, anyone who can reach an instance can create pastes; content stays protected by zero-knowledge encryption, with keys in the URL fragment. The roadmap lists accounts, analytics, tracking, and application-state cookies as non-goals. For a private instance, the docs walk through putting it behind Cloudflare Access instead. Second, there is no Docker image yet. The Cloudflare edition is the one that is ready; a Docker edition is future work, not a current deployment path.
The remaining limitations are documented in the README's honest-limitations section rather than discovered by users: burn-after-reading is an honest-server property (a malicious operator could retain a copy, as with any server-side pastebin); the operator serves the JavaScript, so a compromised deployment could steal keys at the client; and a wrong password on a burn link still destroys the paste, because reading is what consumes it. These are properties of the zero-knowledge pastebin model, not bugs introduced by the rewrite.
Try it, then deploy your own
The preview is at https://binflare.199595.xyz. If you want the full limits, deploy your own from the button in the repository README: https://github.com/openshiye/binflare. Issues are the backlog, and compatibility questions are welcome there.

Top comments (0)