guides
Self-hosted email for agencies: a per-client white-label playbook
May 17, 2026

You want an email stack per client: isolated, on infrastructure you control, branded as your client's product, and without a per-client SaaS invoice eating your margin every month.
Agencies end up reselling email whether they planned to or not. A client needs transactional sends, then a newsletter, then a shared inbox, and suddenly you are the line item that forwards a Resend bill, a Mailchimp bill, and a Front bill, marking them up a little and owning none of it. This is a playbook for the other model: deploy an isolated, white-label email stack per client on your own Cloudflare account, own the relationship, and keep the spread.
The honest version up front, because this post is for engineers who will hold us to it: today this is one deploy per client, not one app serving every client. That turns out to be a feature, not a workaround, and the bulk of this guide is about why and how. Where the architecture is still a design rather than shipped code, this guide says so plainly.
What is the agency problem with email SaaS?
The agency problem is that email SaaS bills stack per client, pool client data in a shared vendor account, and erode the margin you are supposed to be making on the service. Every client multiplies your supplier invoices, and none of the resulting infrastructure is yours.
Walk a typical roster of ten clients. Each one wants transactional email (a sender), a newsletter (a marketing tool), and ideally a place for the team to read replies (an inbox). On the rented model that is three vendors per client, often three bills, frequently priced per contact or per seat so the cost climbs as the client succeeds. You mark it up, but you are a pass-through: the deliverability reputation, the subscriber list, and the message archive all live in the vendor's account, under the vendor's terms, keyed to your agency login.
Two failure modes follow. First, your margin is capped by the vendor's pricing, and per-contact plans mean your best-performing clients are your thinnest margins. Second, all your clients share blast radius. A vendor outage, a policy change, or a reputation problem from one tenant in a shared pool can touch the others. When a client leaves, you cannot cleanly hand them their stack, because there is no stack: there is a slice of your vendor account that you now have to export, lossily, and tear out.
What is the Tinbox model for agencies?
The Tinbox model is one isolated, brand-scoped deploy per client, where many such deploys live in a single Cloudflare account you own. Each client gets their own database, storage, queues, and Worker, named on a tinbox-<org>-<service> convention so resources never collide.
This is per-deploy isolation, not row-level tenancy in a shared database. There is no big clients table with a tenant_id column that everything filters on. Client A's mail is in tinbox-clienta-db (a D1 database); client B's is in tinbox-clientb-db. The same goes for R2 buckets, Queues, and the Worker itself. Picture it as one account holding N self-contained stacks:
Your Cloudflare account
├── tinbox-clienta-* (D1 + R2 + Queues + Worker) → clienta.com
├── tinbox-clientb-* (D1 + R2 + Queues + Worker) → clientb.com
├── tinbox-clientc-* (D1 + R2 + Queues + Worker) → clientc.com
└── ... (one isolated brand-scoped deploy per client)
That diagram is the architecture, not a metaphor. There is no shared multi-tenant database underneath. To understand the resource-naming and many-tenants-per-account mechanics in detail, see the deploy mechanics: brand-scoped naming, many tenants per account, and the tips for running multi-tenant email on one Cloudflare account collect the operational lessons from doing it at scale. This guide stays at the playbook altitude: how to run it as a business.
One thing to be precise about: a single instance serving many client domains (multi-domain-per-instance) is designed but not built. Do not architect your agency offering around it yet. The shipping, supported unit is the brand-scoped deploy.
Why is per-client isolation a selling point?
Per-client isolation is a feature you can sell, not a limitation to apologize for. Because each client is a separate deploy, there is no cross-client data, no shared reputation pool, and offboarding is as clean as handing over a single self-contained stack.
Concretely, the isolation buys you four things:
- No cross-client data. A bug, an export, or a compromised credential is scoped to one deploy. You can tell a privacy-conscious client, truthfully, that their mail lives in a database no other client can reach.
- Per-client sending reputation. Each deploy sends from the client's own domain with its own auth records. A client who sends sloppy campaigns cannot drag down a client who sends clean transactional mail, because they do not share an IP pool or a domain.
- No noisy-neighbor. Volume spikes, storage growth, and queue backlogs are contained per deploy. One client's Black Friday blast does not throttle another client's password resets.
- Clean offboarding. When a client leaves, or wants to take ownership in-house, you hand them the deploy: the code, the D1 database, the R2 bucket, the DNS. There is no lossy CSV export and no data left behind in a shared vendor account. This is the strongest retention-and-trust argument an agency has, because it removes the lock-in objection entirely.
That last point flips the usual SaaS dynamic. Most vendors retain you through switching cost. Owning a portable per-client stack means you retain clients by being good, not by being hard to leave, which is a better business to run.
How do you deploy the stack for a new client?
You scaffold a new client with a single command, scoped to that client's org slug so every resource is namespaced and isolated. The bootstrapper provisions the per-client stack and prints the resource names it created.
npx tinbox init clienta.com --org clienta
The --org flag is what scopes the deploy. It produces output along these lines:
tinbox init: clienta.com
org: clienta
creating D1: tinbox-clienta-db ok
creating R2: tinbox-clienta-mail ok
creating Queue: tinbox-clienta-send ok
deploying Worker: tinbox-clienta ok
→ next: add DNS records for clienta.com (MX, SPF, DKIM, DMARC)
If you forget --org, it does not fail loudly: it defaults the org slug to the one derived from the domain, which is usually fine but can surprise you in scripts. The derived slug may not match the client-id convention your other deploys follow, so you end up with one client named off-pattern. The fix is to be explicit and consistent from the first deploy:
# Set the org slug deliberately, matching your client-id convention
npx tinbox init clienta.com --org clienta
Pick an org-slug convention (we use the client's short id) and never rely on the domain-derived default in scripts. After the deploy, you add the client's DNS records for their domain (MX for inbound routing, plus SPF, DKIM, and DMARC for sending). Each client domain is configured independently, which is exactly the per-client isolation you are selling. For the full first-run walkthrough, see the one-command stack walkthrough.
How do you white-label the stack per client?
You white-label by theming the inbox and console and pointing every surface at the client's own domain. The package ships the UI and auth, so branding is configuration, not a separate build per client.
The branding lives in the per-client deploy's config. A minimal theme override looks like this:
// clienta/tinbox.config.ts
import { defineConfig } from "tinbox/cf";
export default defineConfig({
org: "clienta",
domains: ["clienta.com"],
deployHost: "mail.clienta.com",
appName: "ClientA Mail",
});
Because the console and inbox are served from the client's own domain (mail.clienta.com), the client and their team see a product that looks like theirs, not a Tinbox-branded surface. Where appropriate, you remove or hide Tinbox-facing affordances so the deploy reads as a first-party tool.
Be honest about the ceiling here: white-label means theming plus client-domain endpoints on a stack that ships the UI. It does not mean a managed reseller dashboard where you flip a switch and a new branded tenant appears in a shared app. Each branded experience is a deploy you stand up. For a deeper treatment of theming and brand-scoped surfaces, see theming the inbox and sender per client.
What is the billing and margin math?
The margin story is a one-time license plus Cloudflare usage at cost on your side, billed to the client as a recurring managed service. You capture the spread between near-flat infrastructure cost and the SaaS-equivalent price the client would otherwise pay.
Lay the two models side by side. On the rented model, a client with a growing list pays a per-contact newsletter tool, a per-email transactional sender, and a per-seat inbox, and those bills rise as the client grows. You mark them up modestly and your supplier cost rises in lockstep, so your absolute margin grows but your percentage margin is pinned by the vendor.
On the owned model, your supplier cost is a one-time license for the stack plus metered Cloudflare usage (D1 reads and storage, R2 storage, Queues operations, Worker requests), which scales with storage and volume rather than with contact count or seats. There is no per-email or per-contact tax on the infrastructure. You bill the client a managed monthly fee that reflects the value (a fully managed, isolated, branded email stack on their domain), and the gap between that fee and your at-cost Cloudflare spend is your margin, and it widens as the client's list grows because your cost does not track their list size.
For the underlying per-email and per-contact arithmetic that drives this comparison, see the per-client cost math versus SaaS. The shape of the argument: SaaS pricing punishes the client's success and caps your margin; owned infrastructure decouples your cost from the client's growth and lets the margin expand. Note that the license is honor-system and soft (it nags, it does not meter or enforce per client), so there is no per-client license metering to pass through or reconcile.
How do you operate many client deploys?
You operate many clients by managing many deploys, which is scriptable but is not yet a single console. The realistic agency setup is a client manifest plus a provisioning loop plus CI, not a multi-tenant control plane.
For provisioning, drive npx tinbox init from a list:
#!/usr/bin/env bash
# provision-clients.sh: stand up a stack per client from a manifest
set -euo pipefail
# client-id domain
clients=(
"clienta clienta.com"
"clientb clientb.com"
"clientc clientc.com"
)
for entry in "${clients[@]}"; do
read -r org domain <<< "$entry"
echo "→ provisioning $org ($domain)"
npx tinbox init "$domain" --org "$org"
done
For ongoing changes (a config update, a redeploy after a stack upgrade), you iterate the same list and run wrangler deploy per client deploy, typically from CI so it is repeatable and audited. Each deploy is a separate target with its own bindings, so your pipeline loops over clients rather than deploying once.
This is where the model gets heavier, and you should plan for it. At three clients the loop is trivial. At thirty, you want a real manifest (in version control), per-client secrets management, and CI that can deploy a subset on demand. None of that is exotic, but it is genuinely N deploys' worth of operational surface. Automate early: the manifest, the provisioning script, and the deploy pipeline are the difference between this scaling and this hurting.
What are the honest limits today?
The honest limits are two, and both are about centralization: there is no single multi-domain control plane, and there is no centralized SSO across client deploys. Both are designed; neither is shipped.
Spelled out:
- Multi-domain-per-instance is a design, not built. One instance does not serve many client domains today. The shipping unit is the brand-scoped deploy, one per client domain. Do not promise a client (or yourself) a single app that holds every client's mail.
- Control-plane SSO is a P3 design, not shipped. There is no central Tinbox console you log into once to manage every client, and no shared identity provider federating across deploys. You authenticate per deploy.
Operationally, this means multi-client management is "many deploys, scripted," as described above, not "one pane of glass." That is a real cost, and it is the right reframe to give a prospect: you are trading a centralized dashboard you do not control for per-client isolation you do. For many agencies that is the better trade, but it is a trade, and pretending otherwise would set up a client for disappointment.
If a centralized console is a hard requirement for your operation today, this model is not there yet, and you should weigh that honestly against the isolation and ownership benefits.
When should an agency not use this?
Skip the per-client self-hosted stack for clients whose email is trivial and for clients who specifically need a polished managed marketing UI with a vendor SLA. The per-deploy model earns its keep on volume, ownership, and isolation, not on novelty.
Concretely, do not deploy a stack for:
- Tiny clients with trivial email. A brochure site that sends one contact-form notification a week does not justify a deploy and a DNS setup. Point it at something simpler.
- Clients who need a managed marketing-team console with approvals and an SLA. If the client's marketing team wants a heavily designed campaign builder, multi-step approval workflows, and a vendor on the hook contractually, a managed SaaS is a better fit than a stack your agency operates. Be honest about which clients those are.
For everyone else (clients with real send volume, data-ownership or residency requirements, a desire to leave a per-contact SaaS, or a need for true per-client isolation), the per-deploy model is a strong offering and a better business for you to run.
The shortcut
If you got this far, the shape is clear: per-client isolation, branded on the client's domain, billed as a managed service, owned on your Cloudflare account. You can assemble that from raw Workers, D1, R2, and Queues yourself. Or you can deploy the packaged stack and spend your time on the client relationship instead of the plumbing.
Deploy this yourself: npx tinbox init <domain>. Start with one client, confirm the isolation and branding fit your offering, then script the roster. The deploy mechanics, the cost model, and the broader product are covered in the stack you deploy per client, and the docs walk the per-client setup end to end.
Frequently asked questions
Is each client's email fully isolated with Tinbox?
Yes. The model is one brand-scoped deploy per client, so each client gets their own D1 database, R2 bucket, Queues, and Worker, named tinbox-<org>-<service>. There is no shared multi-tenant database pooling client data. Isolation is at the deploy boundary, which is stronger than row-level tenancy in a shared vendor account: one client's data, reputation, and config never touch another's.
Do I need one deploy per client, or can one instance serve many client domains?
Today you need one deploy per client domain. A single instance serving many client domains (multi-domain-per-instance) is designed but not built. The supported and shipping model is brand-scoped deploys, many of which can live in one Cloudflare account. Frame multi-client as many deploys, scriptable, not one multi-tenant app.
Can I manage all my client deploys from one central console with SSO?
Not yet. A control-plane with centralized SSO across client deploys is a design, not a shipped feature. Today you operate N deploys, which you manage with scripts and CI (a client list, a loop over npx tinbox init, and wrangler in your pipeline). There is no single pane of glass that logs you into every client at once.
How does self-hosted email save an agency money versus a SaaS?
You pay a one-time license for the stack and then Cloudflare usage at cost (D1, R2, Queues, Workers), instead of a recurring per-client SaaS subscription that grows with contacts and seats. You bill the client for the managed service and keep the spread. There is no per-email or per-contact pricing on the infrastructure, so a growing client list does not inflate your supplier cost the way it does on a per-contact plan.
When should an agency NOT use a per-client self-hosted stack?
Skip it for tiny clients with trivial email (a contact form that fires one notification is not worth a deploy) and for clients who specifically need a polished managed marketing-team UI with campaign approvals and a vendor SLA. The per-deploy model rewards clients with real volume, data-ownership requirements, or a desire to leave a SaaS, not clients who send ten emails a month.


