Multi-tenant transactional email: domains, branding and action links without duplicating services
Transactional email looks small until you have more than one product.
With one product, configuration is usually global:
from = Product <no-reply@mail.product.example>
appUrl = https://app.product.example
tenantId = product-slugAs soon as the same auth system creates users for another product, that global setup starts to fail.
The concrete case was Ultimate Porra.
If I invite someone to a World Cup pool and they receive an email from the wrong product, with a link that lands on a shared portal instead of the product they expect, the flow may be technically valid and still wrong. The user does not know where they are, does not recognize the brand and loses trust in the link.
ImportantIn transactional email, branding is not decoration. It is part of the perceived security of the flow.
The real problem
The initial flow had this risk:
The bug was not in Resend or in the template. It was in the contract between producers and the notification service.
The notification service already knew how to send per tenant. But the auth service needed to say which tenant belonged to each action.
The correct architecture
The separation I want is:
Each piece has one responsibility:
| Piece | Responsibility |
|---|---|
| Auth service | Know which product an auth action belongs to |
| Action token | Store product metadata for password setup/recovery |
| Notification service | Render and enqueue by tenant |
| Resend API key | Authorize sending for a domain |
| Domain DNS | Prove SPF, DKIM and DMARC |
appUrl |
Take the user to the right product |
The key is not duplicating the entire service per product if only the brand changes.
Minimum contract per product
Each product needs an explicit configuration entry:
{
"tenantId": "product-slug",
"productCode": "product-slug",
"brandName": "Product Name",
"fromAddress": "Product Name <no-reply@mail.product.example>",
"replyTo": "support@product.example",
"appUrl": "https://product.example",
"authActionBaseUrl": "https://auth.example",
"sendingKeySecretRef": "domain-scoped sending key"
}The important detail: authActionBaseUrl can remain shared.
I do not need one auth service per product if the action page knows what product the token represents.
Action tokens with metadata
The good flow becomes:
create-user action
productCode = product-slug
auth service
-> creates pending user
-> creates password_setup token
-> stores metadata:
tenantId = product-slug
productCode = product-slug
brandName = Product Name
appUrl = product URL
-> enqueues email in the notification service with the product tenantThen, when the user opens the link:
read action token
-> reads token metadata
-> renders page with product branding
complete action
-> consumes token
-> activates password
-> returns the product redirect URLThis solves two problems at once:
- the email is sent from the correct tenant;
- the password page and final redirect belong to the correct product.
Changing the sender is not enough
Only changing fromAddress leaves the flow inconsistent.
| Element | If it stays global | What breaks |
|---|---|---|
fromAddress |
Notipad | The user does not recognize the invitation |
| template | Notipad | The product promise is wrong |
| action page | Notipad | The user thinks they are creating another account |
| final redirect | shared portal | The user lands outside the app |
| support URL | Notipad | Support and trust are mixed |
Multi-tenant email does not mean "several senders". It means a complete product flow.
Sending domains
For production, I prefer one sending subdomain per product:
mail.product-a.example
mail.product-b.exampleThat isolates reputation, intent and DNS.
Minimum verification:
| Record | Purpose |
|---|---|
| SPF | Authorizes servers to send for the domain |
| DKIM | Signs the email and proves it was not altered |
| DMARC | Defines policy and reporting if SPF/DKIM fail |
Resend recommends verifying owned domains and using subdomains to isolate sending reputation. Once SPF and DKIM are verified, DMARC adds another layer of trust.
Free vs Pro is not only volume
For development or a very small private beta, Free can be enough.
But once there are two real products, real users and password/recovery emails, the cost of emails not being sent or hitting uncomfortable limits matters more than the subscription price.
My rule:
Free -> development, tests, very small closed beta
Pro -> production with real users and more than one domain/productNot because Pro is magically safer. Because production needs less friction: more monthly margin, no Free-plan daily sending limit and room to grow without changing architecture during launch.
The exact numbers can change; the operational decision should always check the live pricing page before buying.
n8n and the email queue
If notifications-api works in queue mode, n8n can act as a simple worker:
The advantage is separating user experience from delivery:
- user creation does not wait on Resend;
- failures can be retried;
- jobs can be audited;
- internal tokens never reach the browser.
The rule: n8n can operate jobs, but it should not know more secrets than necessary.
Checklist for adding a new product
Multi-tenant email checklist
- Create sending subdomain:
mail.<product>.com. - Verify the domain in Resend.
- Add SPF and DKIM in Cloudflare DNS.
- Add DMARC, even with a gradual policy.
- Create a sending API key scoped to the domain.
- Store the sending key in the server-side secret store.
- Add tenant configuration to server-side settings.
- Add the product to the allowed auth-action configuration.
- Make the producer send
productCodeortenantId. - Test
auth_password_setup. - Test
password_recovery. - Confirm the password link shows the correct brand.
- Confirm
completeredirects to the correctappUrl. - Review queue logs and Resend status.
The remaining pattern
I do not want one email service per product.
I want one shared service with clear boundaries:
product selects tenant
tenant selects domain, brand and provider key
token keeps context
email and redirect preserve continuityThat keeps the architecture small without making it confusing.
The signal that the design works is simple: a user of Ultimate Porra never has to wonder why Notipad is asking them to create a password.
Technical sources
- Resend documents that domain verification requires SPF and DKIM, and recommends subdomains to isolate reputation and sending intent: Managing Domains.
- Resend allows API keys with
Sending accessand domain restriction: API Keys. - Resend explains DMARC as a layer on top of SPF and DKIM to protect against spoofing and improve trust: Implementing DMARC.
- Resend keeps current pricing information in its documentation and points to the public pricing page for live figures: What is Resend Pricing and Pricing.
This article is part of my architecture and operations notes. If you are interested in this work, follow the blog or contact me.