Eight years of a Slack product, and the email service we had to build to run it

Prosvita saves links before they scroll away. Inbound is the email-to-webhook service it runs on — a second product that existed because the first one needed it.

Product
Prosvita, and Inbound underneath it
Sector
SaaS / internal tools
Running since
2018 — present
Stack
Ruby on Rails and PostgreSQL; Inbound runs on Cloudflare Email Workers, Queues and D1
Our role
Built both, run both

The problem

A link goes past in a busy channel and is gone. Chat is worst at exactly the thing teams most want from it: finding something again a year later. The link was posted, somebody said thanks, and the search box will not bring it back because nobody remembers the words around it.

What we built

Somebody saves the link without leaving the conversation, and it lands on a shelf the whole company can search.

The part worth reading is underneath. Giving every collection its own email address meant assembling SES, Lambda, S3, a MIME parser, a retry queue and a dashboard for answering "why didn't my webhook fire?" — and then maintaining all of it. So we built Inbound instead. Mail sent to an address you own arrives at your code as parsed JSON, signed with HMAC-SHA256 and protected against replay, delivered across three attempts at t=0, t+1m and t+15m with jitter, and logged attempt by attempt with the status code, a response excerpt and the timing. Any delivery can be replayed from the interface.

Prosvita calls it over HTTP, through two service objects, exactly the way any other customer would. There is no private door.

That is the same judgement a client is buying: when the plumbing is going to be needed twice, build it as a service with a boundary rather than as a feature with a tangle. The boundary is what made the second use cheap, and what made it testable.

How big this actually is

Prosvita is a small product, and it is worth saying so. It is in beta: free, or pay what you want, with no credit card and no per-seat pricing. Overstating it would undermine the other two studies.

Inbound is the half with numbers attached to it, and they are targets rather than measurements — a delivery budget the queue was designed against, not a window anybody has since gone back and audited. So they are not printed here. What can be said without a dashboard is structural, and it is the part that matters at three in the morning: a delivery that fails is retried on a schedule instead of dropped, every attempt is recorded with its status code, its response excerpt and its timing, and any delivery at all can be replayed by hand from the interface. The question "why didn't my webhook fire?" has an answer rather than a shrug, which is the entire reason the service exists as a service.

Operating it since 2018

Eight years, one framework, and a codebase that has outlived the versions of everything it was written against. Nothing here was rewritten because it was fashionable to rewrite it.

The migrations that cost the most were the ones with no feature attached to them. The version manager swapped out in 2020. Rails 7 in 2022, which changed default headers and so changed behaviour nobody had written down anywhere. AWS credentials moved into a secrets store in 2024, replacing an arrangement that had worked fine and would have kept working fine until the day it did not. Postgres to 18 in 2026.

Each of those was a stretch of work that produced nothing a user could point at, and skipping any of them would have produced a rewrite two years later. That is the whole trade, and it is the one a client is buying when they ask who is going to look after this in year three.