One of our junior developers came to us with a notifications problem and a heretical answer. The problem: a content site whose pages update on their own schedule, and readers who wanted a ping when a page they cared about changed — proper browser push, no accounts, no app, no email address handed over. The heretical answer: don't build a backend at all. Use an email inbox as the database.
The short version. It worked — in production, for real readers. Subscriptions arrived as email. A scheduled job read the inbox as its only store of truth. Readers got real push notifications out the other side. There was no database server, no backend to provision, and no vendor account for the push side either. It also had honest limits: latency measured in minutes, and a database you query by re-reading history. So it has now graduated to a proper backend — which was always the deal. An experiment's job is to prove the shape of the real system. This one did, and the shape is worth stealing, so here is the write-up.
How it worked
Four moving parts, none of them a server we ran.
Subscribe. The button asked the browser for notification permission and created a standard web-push subscription — an endpoint URL and a pair of keys. Then, instead of posting that to an API, the page posted it as an ordinary form submission to a free form-to-email relay, which turned it into an email and delivered it to a dedicated inbox. One inbox, one job: nothing else was ever sent there.
The mailbox was the store. It was never emptied. Every subscription that ever arrived stayed in it — an append-only archive. The pipeline didn't delete what it had processed; it kept a watermark, a single processed-up-to-here marker, and only ever read mail past the mark. Rewind the watermark and the whole history replays; merges dedupe by push endpoint, so replaying is safe. That one trick turned an inbox into an event log with disaster recovery built in.
Lists. A scheduled job woke up when content changed, read the new mail past the watermark, and folded each subscription into the list it named: watch this page, or hear about new posts. One file per list, keyed by endpoint. Unsubscribing needed no form and no button: block the site in the browser, and the next push to the dead endpoint comes back 404 or 410 from the push service — the pipeline pruned it. The database healed itself.
Detect and send. The same job hashed the watched pages, and when a hash moved, it waited until the new version was actually live before sending — a notification for a page nobody can see yet is a bug with good timing. Then it sent the pushes itself, using VAPID, the open web-push protocol, signed with a keypair of its own. No push vendor, no account, no per-message fee. And a page seen for the first time was baselined silently, so nobody ever got blasted about old content.
Why it was genuinely good
It is easy to laugh at "the database is an inbox" until you notice what email gives you for free.
Email is the most universal write API ever deployed. Anything that can send mail can write to this database — a web form, a script, a human forwarding a message. The mail system has spent forty years solving delivery, retries, and queuing, so the experiment didn't have to solve any of them.
It is durable and legible. The entire state of the system was a mailbox a person could open and read. No dump format, no export step, no binary only an ORM understands. When something looked wrong, you debugged the database the way you read your mail — because it was mail.
Zero provisioning. The whole backend was an inbox that already existed, a relay that needed one activation click, and a scheduled job. Nothing to size, patch, back up, or pay for. For a junior developer's project, that is not a shortcut. That is the correct amount of infrastructure.
Where it creaked
Latency. A subscription took a relay hop and an ingest cycle to become real; a notification waited on the pipeline's schedule. Minutes, not moments. Fine for "this page changed." Wrong for anything urgent.
Indirection. A mailbox never answers a question; it only tells a story. Every answer — who is subscribed to what, how many, since when — meant re-reading history and folding it into shape. Cheap at dozens of subscribers. Silly at thousands.
Schema in prose. A subscription arrived as form fields in an email body. Parsing mail as a database works right up until a relay changes its template — and then it doesn't.
A stranger in the middle. The free relay was a dependency with deliverability moods of its own. One more system whose bad day became ours.
The graduation
The experiment has been retired the way experiments are supposed to retire: it worked, and it taught us the shape of the real system. By the end, every hard question had a proven answer — how subscribing should feel, what a list means, when a notification is allowed to fire, how dead endpoints get cleaned up. The replacement kept all of it: same buttons, same detection, same push. Only the store changed — a proper backend where a subscription is a row, written the moment it happens, instead of a letter the system reads later.
That is the deal we make with experiments here. The inbox was never the destination; it was the cheapest possible way to find out what the destination had to be. And the junior developer's heresy turned out to be a design tool worth keeping: when you don't yet know the shape of a system, build it out of the most boring, durable, readable thing you already have — and let it teach you what to build properly.
← All field notes