Back to blog
Hand Someone a Page

Hand Someone a Page

developer-tools open-source cloudflare software-engineering

Sharing a document is weirdly hard.

Not publishing a site. Not collaborating. Just: I have a page — a plan, a report, a review — and I want you to read it. Every path is wrong in its own way. Paste it into chat and the formatting dies. A gist renders markdown but mangles HTML. Notion wants both of us to have accounts and me to think about permissions. A pastebin puts my document on someone else’s domain with someone else’s lifetime. Deploying an actual site for one file is ceremony.

So I built handbill. One command:

$ handbill plan.html
https://a3f9c1d4e2b8.yourdomain.dev

One file in, one link out, on a domain you own. npm i -g handbill, MIT, self-hosted on Cloudflare’s free tier in about ten minutes.

The command is simple. The design decisions underneath it are where the fun is.


The URL is the file

That subdomain isn’t random — it’s the first 12 hex characters of the file’s SHA-256. The URL is the content.

This one decision buys almost everything I wanted:

Links are immutable. The page behind a link can never change, because a changed file has a different hash — a different link. If I hand you a review on Tuesday, what you open on Friday is byte-for-byte what I sent. Nobody can quietly edit the thing you already approved.

Publishing is idempotent. Same bytes, same link, nothing stored twice. Run it as many times as you want; there’s exactly one URL for that document, ever.

Versions are free. Revise the file, publish again, get a new link. The old one keeps working. Your document history is just… links.

The server can verify everything. The client computes the hash for the URL; the server recomputes it from the bytes and rejects a mismatch. You cannot publish content under a hash it doesn’t have.

And because a page can never change, it’s served with a one-year immutable cache header. Pages are fast for the same reason they’re trustworthy.

Unguessable, not secret

Each link carries 48 bits of hash. Pages are served with noindex, nofollow, so search engines never see them. Guessing a live link means brute-forcing a keyspace of 2⁴⁸ through a rate limiter.

But I want to be precise, because this is where tools usually oversell: the link is unguessable, not secret. Anyone you hand it to can read it and forward it. TLS protects it in transit, and nothing else does. That’s the whole access model — link-knowledge is the ACL — and the docs say so in plain words instead of implying encryption that isn’t there.

There’s one more quiet security property: every page lives on its own subdomain, which means every page gets its own browser origin. One published page can’t read another’s storage, cookies, or content. The isolation isn’t a feature I built; it’s the web’s own sandbox, aimed correctly.

handbill is self-hosted first: one Cloudflare Worker, one R2 bucket, two DNS records, in your account. Free tier covers personal use. There’s no service in the middle, no telemetry, nothing that phones home.

The consequence I care most about: your links outlive the project. If I abandon handbill tomorrow, every page you ever published keeps serving, because it was only ever your Worker reading your bucket on your domain. That’s the difference between a tool and a dependency on a company.

Names get the same treatment. A hash link is permanent, but sometimes you want plan.yourdomain.dev to always show the latest plan. Aliases do that — the name serves whatever page you point it at, while every old hash link stays exactly what it was. And because a name is guessable where a hash isn’t, the feature is opt-in and the docs spell out the trade.

Small on purpose

The entire server — storage, auth, hashing, host routing, aliases, serving — has a hard budget: 750 lines of source, enforced by a CI job that fails the build over it. It currently sits at 738.

The budget isn’t minimalism for its own sake. A self-hosted tool is code you’re asking strangers to run in their own cloud account, and every line is something a stranger has to trust. Small enough to read in one sitting is the security model, and the audit story, and the contributor story. When the budget binds, a feature has to pay for itself by removing something else — which turns “keep it simple” from a value into a test that fails.

It’s MIT, and the repo is built to the same line: one file per service, and a test suite that runs entirely on in-memory layers — bun test, no Cloudflare account needed to change the Worker safely.

The CLI holds the same line from the other side: two dependencies, and a strict stdout discipline — success prints exactly the URL and nothing else, errors go to stderr, --json everywhere. Which brings me to who that discipline is really for.

The last line of an agent’s work

I built handbill because my AI coding agents kept ending tasks with “I wrote the plan to plan.html.” A file, on a machine, is a dead end — the deliverable I actually wanted was a link I could open, forward, and read on my phone.

So the repo ships an agent skill: drop it into Claude Code or any agent that reads AGENTS.md-style skills, and agents end their work by publishing the page and printing the URL as their last line. The stdout discipline exists precisely so that line is machine-parseable: one URL, nothing else, every time.

Markdown is first-class for the same reason — agents and humans both write it. The CLI renders it to a self-contained page with a light/dark stylesheet before publishing; the server only ever sees finished HTML bytes.

It’s already recursive at home: the architecture doc for handbill’s next release is published with handbill, and the GitHub milestone links to the immutable URL. The design review can’t drift under its reviewers, for the same reason none of your links can.

What it deliberately doesn’t do

No accounts, no analytics, no view counts, no comments, no multi-file sites, no dashboard. Each of those is a reason to run a server that knows things about people, and the entire point is a server that knows nothing — it stores bytes under their own hash and serves them back.

What’s next is written down in public: a hosted tier, so people without a domain can get a link after a login — same code, same invariants, links still immutable. But self-hosting stays the first-class path, because “your links outlive the project” only means something if you hold them.

Hand someone a page: handbill.dev · GitHub