You are one person. Mail arrives at support@, and each message costs you fifteen minutes — not because the answer is hard, but because you have to work out who sent it, what they bought, whether they are in a trial, and whether this is urgent before you can even read it properly.
Support is the employee a solo developer cannot hire. And every piece needed to build it was already in the catalog.
Mail lands in your database
Messages arrive through Cloudflare Email Routing, get parsed — multipart and attachments included — and land as rows in your own D1.
Threading is on In-Reply-To and References, the actual mail headers, so a conversation stays one conversation across replies rather than becoming five unrelated tickets because someone changed the subject line.
Classified three ways, on your own binding
Each message is classified along three axes at once: category, priority, and sentiment.
Three axes rather than one label is what makes an inbox sortable instead of merely tagged. “Billing” is a folder. “Billing, high priority, angry” is a queue position — it tells you what to open first, which is the only question you actually have at 9am.
Eight categories ship, and the taxonomy is federated: add your own with defineSupportCategories and they sit alongside rather than in a parallel scheme.
The inference runs on your own Workers AI binding. Two consequences, and both matter:
- The cost lands on your bill, so there is no per-ticket pricing between you and answering your customers.
- Your customers’ support mail never leaves your infrastructure. Nobody’s angry email about a billing error becomes training data or a vendor’s log line.
Everything derives from immutable mail
This is the design decision that makes the rest safe.
The mail itself is immutable. Everything else — the classification, the priority, the thread structure — is derived from it.
So a wrong classification is not repaired. It is recomputed. Improve your categories, change the model, add a rule, and re-derive: the corrections apply to your whole history rather than to messages you happen to touch from now on.
The alternative — mutable ticket state that people hand-edit — means your data is the accumulated result of every past decision, correct and otherwise, and you can never re-run anything.
What it knows about the sender
The part that saves the fifteen minutes.
Each sender is linked to the account and purchases your app already knows about. This is not an integration you configure; the data is already in your database, because payments and auth put it there.
So the message arrives with context attached: who this is, what they are entitled to, what they paid and when. You open the mail already knowing whether you are talking to a trial user or someone eighteen months in.
That is the thing hosted helpdesks charge the most for, and the reason it is expensive there is that they have to integrate with your billing system. Here they are the same database.
What it is not
It is not a helpdesk. There is no assignment, no status workflow, no SLA timers, no agent seats. Those exist because helpdesks serve teams, and a team needs to know who picked something up.
If you have five support agents, buy a helpdesk. This is for the case where the answer to “who is handling it” is “you”, and the entire value is in triage.
Replies go out through the email capability, carrying your domain, your DKIM signature, and correct threading headers — which only the Worker can set. Reply from your own mail client and the thread breaks; reply through the capability and it holds.
The admin surface is control-plane only and default-denied, so the inbox is not a route sitting on your public API waiting to be found.
Why it belongs in a backend kit
Because the expensive part of support tooling is not the inbox. It is knowing who is writing to you.
Every hosted tool solves that with an integration against a billing system it does not own, which is why it is a paid tier and why it breaks. When your mail, your accounts and your purchases are three tables in one database, the integration is a join.