← all posts

A URL Shortener Without a Database Is Still an Operations Problem

Removing a database moved the operational boundary. It did not remove it.

A URL shortener looks like a database problem: a code maps to a destination, a request looks up the code, and the service returns a redirect.

That model makes the conventional implementation obvious. Run a web service, give it a table and an index, then add a control plane for creating records. It also makes the service the owner of every link.

I wanted the redirector to be replaceable and the record to outlive a deployment. Urlattice publishes signed link records to public Nostr relays, uses a Cloudflare Worker to discover and verify them at request time, and keeps the creator as a static NIP-07 page on GitHub Pages. The complete source is at github.com/argakiig/urlattice.

Getting the first redirect working was quick. Making the result worth operating took longer.

The record is the data plane

Each short link is a signed Nostr event containing a normalized HTTP(S) destination, a random nonce, and a version. Its short code comes from the first 80 bits of a SHA-256 digest over the canonical payload and public key, then travels with the event as an indexed Nostr tag.

The Worker asks three relays for events with that tag. It verifies the Nostr signature, the exact canonical payload, the destination scheme, and the derived code before returning a 302. No valid record is a 404. A total relay outage is a 503. Two valid destinations for one code fail closed with 409.

The redirect domain must stay online, and at least one relay must retain the event. Urlattice can replace the Worker without migrating a link table because the records live elsewhere.

The creator is an authorization boundary

Public relays solve replication. They do not decide who may create a resolving link for a domain.

Authorization starts with one configured Nostr public key. Anyone can publish an event to a relay, but the Worker resolves only a correctly signed event from that key. Copying the frontend provides no ability to create a resolving link.

In the browser, NIP-07 sends the signing request to an extension without exposing the private key to the page. A terminal publisher covers the cases where a browser is the wrong tool. It prompts for an nsec or hex key without echoing or storing it and verifies the derived public key against the configured one. A URL is printed only after every relay accepts the event; replication is part of creation rather than a background hope.

The security review changed the implementation

Production-shaped failure modes remained around every external boundary of the initial implementation.

Relays are untrusted network peers. A malicious or broken relay can send malformed JSON, oversized messages, or an unbounded stream of matching events. The resolver now caps message size and the number of retained events per relay, turns malformed traffic into a relay error, and makes completion idempotent. That moves an outage back to 503 instead of letting one relay consume Worker time and memory.

GitHub Pages presented a different boundary. It is an excellent static host and a poor place to define response headers, while a page capable of requesting wallet signatures should never run inside somebody else's iframe. The console now refuses to initialize when framed, and Cloudflare adds frame-ancestors 'none', X-Frame-Options: DENY, Referrer-Policy: no-referrer, and X-Content-Type-Options: nosniff in front of the custom domain.

An earlier terminal creator silently generated a separate private key and persisted it under the local configuration directory. Besides creating a second key lifecycle, it could publish records the Worker would never resolve. Removing it was simpler than repairing the wrong interface; the explicit prompt-only version came later.

CI received the same scrutiny. Action revisions are pinned, while write and identity-token permissions exist only on the Pages deployment job. Those constraints make the deployment authority visible in the workflow instead of leaving it implicit.

What free actually buys

On the cost side, the redirect Worker fits within Cloudflare's free plan, the creator runs as a static GitHub Pages site, and public relays carry the records. No managed database, queue, key vault, or API service appears in the bill.

A registered domain still costs money, and free hosting does not remove operational responsibility. Cloudflare needs DNS and routes. GitHub Pages needs custom-domain verification. Public relays can rate limit, prune, or fail. Those dependencies are explicit and replaceable.

For a personal shortener, that trade is worthwhile. Scope does most of the work: one key creates immutable links, with no edits, analytics, custom aliases, or abuse workflow. Every omitted feature removes state that would otherwise need an owner.

The one-line version

A database-free shortener is possible, but the real design work is deciding which boundaries still own the data, authority, and failure modes.