Skip to content

guides

How to build a white-label email platform for each client

May 29, 2026

#white-label#agency#multi-tenant#branding#cloudflare
How to build a white-label email platform for each client
Visual: Bureau Morandi on okslop

Agencies keep getting asked for "email, but with our client's logo on it." The cosmetic part is easy. The part that bites is multi-tenancy, sending reputation, and not paying a per-client SaaS markup that eats the margin.

White-labeling email sounds like a theming task and ends up being an infrastructure decision. A client-branded inbox with the wrong logo is embarrassing; a client-branded inbox where one client's query accidentally returns another client's mail is a breach. This guide walks the whole thing the honest way: what actually has to be branded, the two real multi-tenancy models and where each one hurts, and how to do it without reselling someone else's bill. At the end we show where a self-contained stack like Tinbox collapses the boilerplate, with the current limits stated plainly so you can plan around them.

What does white-labeling email actually require?

White-labeling email means branding three separate layers, and only one of them is cosmetic. People focus on the visible UI and forget the two that determine whether the product is actually the client's.

  1. The sending domain. Mail must leave from the client's domain, authenticated by the client's own SPF, DKIM, and DMARC. This is the layer that matters most: it decides deliverability, who owns the sending reputation, and whether a recipient's mail client shows the client's domain or yours. Get this wrong and the logo is irrelevant because the mail is in spam.
  2. The webmail interface. The inbox UI the client's team logs into: logo, color theme, app name, favicon, login page, the URL in the address bar. This is the layer clients see, so it gets the attention, but it is the least load-bearing.
  3. The email templates. The from-name, reply-to, signature, footer, and unsubscribe link on every message the client sends. A perfectly themed UI that sends receipts signed "Powered by YourAgency" is not white-labeled.

If you only brand the UI, you have a skinned demo. A real white-label setup brands all three, and the sending domain is the one that takes engineering, not CSS.

One shared deployment or one per client?

Use one deployment per client unless you have a specific reason not to. This is the single most important architecture decision, and the cheap option is the one that quietly creates liability.

There are two models. Shared multi-tenancy puts every client in one database, separated by a tenant_id column on every table and a WHERE tenant_id = ? on every query. Per-client deployment gives each client their own database, their own domain, and their own running stack.

ConcernShared (tenant_id column)Per-client deployment
Data isolationOne missing WHERE clause leaks mailPhysical: separate databases
Domain reputationShared pool; one bad sender hurts allIsolated per client
Blast radiusOne bad migration breaks everyoneOne client at a time
Cost at small scaleCheapestSlightly more (per-deploy overhead)
Per-client customizationConditional logic everywhereNatural, per-deploy config
Operational complexityOne stack to runMany stacks to run

The shared model is genuinely cheaper to operate and easier to update in one place. Be honest about why that is tempting and why it is dangerous for email specifically. Email is not analytics events: it is private correspondence and password resets. The failure mode of a forgotten tenant filter is not a wrong chart, it is Client A reading Client B's customer mail. And sending reputation does not isolate in a shared pool, so one client running a sketchy campaign degrades deliverability for every other client on that domain or IP range.

For an agency, the per-client model is almost always right. The overhead of "more deploys" is a script. The cost of a cross-tenant leak is the agency. If you want the business-side framing of why owning this is worth it, the agency email margin playbook makes the case in dollars.

How do I brand the sending domain per client?

For each client, publish their own MX, SPF, DKIM, and DMARC records so outbound mail authenticates as the client and inbound mail lands in their stack. This is per-client DNS, not shared infrastructure, and it is what makes the mail genuinely theirs.

Each client domain needs four record types. Here is a representative set for one client:

# Inbound: where mail for the client lands
clientdomain.com.            MX    10  mx.clientdomain.com.

# SPF: authorize the sender (one record, exactly one v=spf1)
clientdomain.com.            TXT   "v=spf1 include:_spf.clientdomain.com ~all"

# DKIM: the client's own public key at a selector
tbx._domainkey.clientdomain.com.  TXT  "v=DKIM1; k=rsa; p=MIGfMA0G...client-key..."

# DMARC: alignment policy, reports to the client (or you, managing)
_dmarc.clientdomain.com.     TXT   "v=DMARC1; p=quarantine; rua=mailto:dmarc@clientdomain.com"

The non-negotiable rule: each client gets their own DKIM key pair. Sharing one signing key across clients defeats the entire point, because the mail is then cryptographically signed by your agency, not by the client. A per-client key means a per-client deployment that holds that key, which is another reason the per-deploy model wins. If you are shaky on any of these records, the SPF, DKIM, and DMARC setup walkthrough covers the syntax and alignment rules.

One platform constraint to plan around: Cloudflare Workers cannot open outbound port 25 for direct-to-MX SMTP. So whatever you build, the actual sending happens over an HTTPS-based path, not a classic SMTP relay you stand up per client. That is a property of the platform, not a Tinbox quirk, and it is why edge email uses an HTTPS sender. The DNS above is still standard email; only the transport under the hood differs.

How do I brand the webmail interface for each client?

Override the logo, color theme, app name, favicon, and login page per deployment, and serve the inbox from a subdomain on the client's own domain. Because each client is a separate deployment, this is configuration, not conditional UI code.

In a hand-rolled stack, you would typically drive branding from a per-deploy config object and a CSS variable theme. A minimal shape:

// brand.config.ts (one per client deployment)
export const brand = {
  name: "Acme Mail",
  logoUrl: "/brand/acme-logo.svg",
  faviconUrl: "/brand/acme-favicon.ico",
  // theme tokens consumed by the UI's CSS variables
  theme: {
    primary: "210 90% 45%",   // HSL channels for hsl(var(--primary))
    accent: "150 70% 40%",
    radius: "0.5rem",
  },
  loginTagline: "Sign in to Acme Mail",
  supportEmail: "support@acme.com",
};
/* theme.css: the UI reads these variables, brand.config sets them */
:root {
  --primary: 210 90% 45%;
  --accent: 150 70% 40%;
  --radius: 0.5rem;
}

A common foot-gun here, learned the hard way: if your theme file is written in one color space (say oklch(...)) but your components expect hsl(var(--primary)), the theme silently does nothing and every client looks unbranded. Pick one color representation for the tokens and the consumers and keep them in lockstep. Mismatched color spaces are the single most common reason a "themed" build ships looking default.

Then serve the inbox from the client's domain. Point a subdomain at the deployment so the client's team logs into mail.clientdomain.com, not a generic agency URL:

mail.clientdomain.com.  CNAME  client-stack.workers.dev.

Now the address bar, the logo, the colors, and the login copy are all the client's. The per-tenant custom domain guide covers the routing details when each client is on their own domain.

How do I brand the email templates themselves?

Set per-client from-name, reply-to, footer, and unsubscribe so every sent message reads as the client's, including the List-Unsubscribe header that bulk senders now require. The UI is what the client's team sees; the templates are what the client's recipients see, and that is the more public surface.

A per-client template config:

export const mailDefaults = {
  fromName: "Acme",
  fromAddress: "hello@acme.com",
  replyTo: "support@acme.com",
  footerHtml: `<p>Acme Inc · 123 Market St · <a href="{{unsubscribe_url}}">Unsubscribe</a></p>`,
  // RFC 8058 one-click unsubscribe, required by Gmail/Yahoo bulk rules
  listUnsubscribe: "<https://mail.acme.com/u/{{token}}>, <mailto:unsub@acme.com>",
  listUnsubscribePost: "List-Unsubscribe=One-Click",
};

Two things that are easy to miss and expensive to get wrong. First, the unsubscribe must be per-client and functional, not a placeholder, or the client fails Gmail and Yahoo bulk-sender requirements and their campaigns start getting blocked. Second, the reply-to has to route back to the client, because a recipient hitting reply on a white-labeled message should reach the client's support, never your agency's. A template that leaks your agency's address in the reply-to is a white-label failure even if every pixel is on-brand.

How do agencies avoid paying a per-client SaaS markup?

Own the infrastructure instead of reselling someone else's metered bill. Reselling a SaaS means your cost floor rises with each client's contact count or seat count, and you mark up a number you do not control. Owning the stack makes the cost your own platform usage, which at small per-client volume on Cloudflare is often pennies, and you bill a flat managed-service fee on top.

The arithmetic is the whole argument. Reselling a per-contact tool, a client with 50,000 subscribers might cost you several hundred dollars a month before you add any margin, and that floor climbs every time their list grows even if their send volume is flat. Self-hosted on Cloudflare, the same client's D1 storage, R2 attachments, and Worker invocations land in the low single digits of dollars per month at that scale, and the marginal cost of their list growing is close to zero. The margin you bill is the value of a managed, branded, owned inbox, not a markup on a metered SaaS line item.

The catch you must price in honestly: this trades a per-client SaaS bill for per-client deployment and management. You are now the operator. The work is mostly automatable (a deploy script, a DNS template, CI), but it is real work, and it is the thing you are charging for.

Where does Tinbox fit for white-label email?

Tinbox is a self-contained email stack you deploy once per client, on your own Cloudflare account, branded per client by a flag. It is the per-deployment model from the table above, made into one command, so the isolation you want for an agency is the default rather than something you build and police.

A client-branded stack is one init:

npx tinbox init acme.com --brand acme
✔ Provisioned D1 database (tinbox-acme-db)
✔ Provisioned R2 bucket (tinbox-acme-mail)
✔ Provisioned Queue (tinbox-acme-send)
✔ Wrote wrangler.jsonc (assets + queue consumer + cron + email handler)

Next: add the printed DNS records (MX, SPF, DKIM, DMARC), then `npx tinbox deploy`.

Everything is namespaced tinbox-<brand>-<service>, so many client stacks live side by side in one Cloudflare account without colliding. Each client gets their own D1 database (hard data isolation), their own R2 bucket, their own queue, and their own DKIM key, which is exactly the separation the shared-tenant model cannot give you. The bootstrapper prints the precise DNS records to publish, collapsing the most error-prone part of this guide into copy-paste, and the UI ships with a theme you override per brand for the logo, colors, and app name. You bill the client a managed fee; your cost is Cloudflare usage. To automate the repeat-per-client step in CI, see deploying the stack from GitHub Actions.

Now the honest caveats, because an agency planning a fleet needs them up front:

  • One deploy is one brand and one domain today. You run many clients per Cloudflare account by doing one init per client domain. Multi-domain-per-instance is designed but not yet built, so several client domains means several deploys, not one shared multi-tenant instance. For the per-client model this guide recommends, that is actually the desired shape, but do not expect a single instance to serve ten client domains.
  • There is no shipped control-plane to manage the fleet from one dashboard yet. Cross-deploy SSO and a single admin pane are designed, not built. You manage deployments today the way you manage any fleet of Workers: scripts and CI.
  • It is a sender, newsletter tool, and webmail inbox, not a Gmail clone. Calendar, docs, and the rest of a productivity suite are out of scope. What you get is owned, searchable, branded email per client.

For role separation inside a client's team (agents, admins, viewers), the team inbox roles guide covers the access model. The white-label mechanics in this guide are durable regardless of where you host; Tinbox is the part that makes the per-client deploy cheap enough to do for every client.

Deploy this yourself: npx tinbox init clientdomain.com --brand client-name. The generated DNS records and the brand theme override are the two things to read next.

Frequently asked questions

How do I deploy a white-labeled, client-branded email stack for an agency?

Brand three layers per client: the sending domain (publish the client's own SPF, DKIM, and DMARC so mail is authenticated as theirs), the webmail interface (logo, color theme, app name, login page), and the email templates (from-name, reply-to, footer, unsubscribe link). For isolation, deploy one stack per client domain rather than sharing one database across clients. With Tinbox, that is `npx tinbox init clientdomain.com --brand client-name`, run once per client, all on your own Cloudflare account so there is no per-client SaaS markup.

Should I use one shared deployment or one per client for white-label email?

One deployment per client is the safer default for an agency. A shared deployment with a tenant_id column is cheaper to run but every query is one missing WHERE clause away from leaking one client's mail into another's inbox, and a single bad migration or rate-limit event hits all clients at once. A separate deploy per client gives hard isolation: separate database, separate domain reputation, separate blast radius. Tinbox uses the per-deploy model, scoped by a brand flag, so each client is fully isolated.

Can clients send email from their own domain with a white-label setup?

Yes, and they should. White-labeling the UI is cosmetic; white-labeling the sending domain is what actually matters for deliverability and trust. Each client publishes their own SPF, DKIM, and DMARC records so receiving servers authenticate the mail as coming from the client, not from your agency or a shared SaaS pool. This also means one client's spam complaints do not drag down another client's domain reputation.

How do agencies make margin on white-label email instead of passing through SaaS costs?

Reselling a SaaS like Mailchimp or SendGrid means you mark up someone else's per-seat or per-contact bill, and the price floor rises with the client's list. Owning the infrastructure flips that: the cost is your own Cloudflare usage (often pennies per client at small volume) plus your setup and management time, and you bill the client a flat managed-service fee. The margin is the gap between flat Cloudflare cost and the value of a managed, branded inbox.

Does a white-label email platform need a separate domain per client?

Yes for the sending and receiving side: each client needs their own domain (or subdomain) with its own MX and authentication records so mail is genuinely theirs. The webmail UI can live on a subdomain you control (like mail.clientdomain.com) pointed at that client's deployment. In Tinbox today, one deploy maps to one brand and one domain, so multiple client domains means multiple deploys, not one multi-domain instance.