A queued email is not a delivery record. For an e-commerce compliance notice, the least complex workable setup is one send path per tenant identity, an immutable notice snapshot, and an append-only stream of provider events. Short answer: choose a transactional email integration only after you can trace one notice from approved content to its recipient and final reported status. A welcome-email demo will not test that boundary.
The receipt is the test.
| Choice | Best fit | Record you still need |
|---|---|---|
| One shared sending identity | Early internal tests | Tenant-specific sender attribution |
| Tenant-scoped sending identities | Storefront notices with distinct senders | Notice snapshot and event history |
| Separate infrastructure per tenant | Strong isolation requirements | Cross-system reconciliation |
Choose tenant-scoped identities when each storefront sends its own notice. Move to separate infrastructure only if its isolation benefit justifies another deployment and reconciliation path. The two deciding criteria are sender isolation and evidence quality, not the shortest setup screen or the smallest quoted unit price.
What should a multi-tenant SaaS transactional email provider prove beyond welcome emails?
A tenant is more than a template variable. Store the tenant ID beside the approved sender identity, recipient, notice version, locale, and delivery attempt ID. Resolve sender configuration from the tenant record on the server; never accept a caller-supplied From address as authority. This blocks a routine integration mistake: a batch worker receives two tenants' jobs and accidentally reuses the first tenant's sender settings.
For domain management, test the full lifecycle: who can request a sending domain, who verifies control, what happens while verification is pending, and whether a removed domain can still be selected by a queued job. Keep that state explicit in your own data model. A provider dashboard alone cannot define your tenant authorization rules. For example, suppose a storefront changes its sender after a notice batch has been approved but before the worker starts. The worker should use the sender identity recorded with the approved attempt, validate that it remains authorized, and stop if it has been revoked. Silently switching to the new identity produces a different record from the one an operator reviewed. This creates more state to maintain, but that state is the point of the audit trail.
Preview matters here. Render the exact approved template and variables before enqueueing; persist the rendered subject and body snapshot or a durable reference to them. Mustache's variable interpolation escapes HTML by default, while unescaped interpolation is a separate operation. That distinction is worth testing with a storefront name containing markup. A preview generated with sample data is useful for review, but it is not evidence of what a particular recipient was sent.
Don't confuse the two.
How do you prove what happened after enqueueing?
Treat acceptance, delivery reports, and a recipient actually reading a notice as different claims. Save every status event with the provider's event ID, timestamp, original attempt ID, and raw payload reference; do not overwrite the previous status with the latest callback. Reordered or repeated callbacks should leave the same audit history. No open-tracking pixel can establish that a person read a legal notice.
Here is a small boundary that makes batch sends inspectable. The transport adapter is deliberately generic; it must return the provider's message ID, and the event receiver must map subsequent reports back to the attempt.
type Notice = {
tenantId: string; recipient: string; sender: string;
subject: string; html: string; version: string; noticeId: string;
};
type Attempt = { id: string; noticeId: string; providerMessageId?: string };
type Store = {
reserve(notice: Notice): Promise<Attempt>;
recordAcceptance(id: string, messageId: string): Promise<void>;
recordFailure(id: string, reason: string): Promise<void>;
};
type Transport = {
send(input: { from: string; to: string; subject: string; html: string;
idempotencyKey: string }): Promise<{ messageId: string }>;
};
async function dispatch(notice: Notice, store: Store, transport: Transport) {
const attempt = await store.reserve(notice);
try {
const result = await transport.send({
from: notice.sender, to: notice.recipient, subject: notice.subject,
html: notice.html, idempotencyKey: attempt.id
});
await store.recordAcceptance(attempt.id, result.messageId);
} catch (error) {
await store.recordFailure(attempt.id, String(error));
throw error;
}
}
The store must make reserve idempotent for a notice version and recipient. Check whether the chosen transport actually honors an idempotency key; the interface does not make that guarantee. A timeout after remote acceptance is ambiguous. Reconcile against the provider message ID or event feed before retrying, or a compliance notice may be sent twice. Unknown is a status, not permission to send again.
What should the rollout test?
Run a batch with two tenants, two approved sender identities, a revoked identity, and one recipient whose address is invalid. Verify that every attempt points to its own immutable content snapshot and that the revoked identity fails before transport. Replay the same delivery event twice, then deliver an older event after a newer one. Your audit view should preserve both the evidence and the chronology without turning the older report into the current state.
Measure time to first verified test send, then measure the work needed to answer one audit question: which approved content went to this address, under which sender, and what did the transport report? Count the configuration objects and manual steps needed to answer it. A quick SDK call is cheap to demo; a clean incident query is harder to fake.
For US commercial email, the FTC's CAN-SPAM guide describes requirements such as accurate header information and an opt-out mechanism. Do not assume every compliance notice has the same legal classification as a promotional message. For EU recipients, have counsel determine the applicable legal basis and retention policy before encoding them as flags in an SDK; the sources below do not establish a universal retention period. Keep suppression policy separate from the transport retry queue.
When does the runner-up win?
Tenant-scoped identities have a limitation: they share an operational failure domain, so they're unsuitable when contracts require independent infrastructure. Separate infrastructure per tenant makes sense when contractual isolation or independent operational control outweighs deployment overhead. It also increases the number of credentials, event feeds, and failure paths the team must reconcile. Benchmark that overhead with a real two-tenant batch and a replayed callback, not a slide about scalability. For an early internal test, a shared identity can reduce configuration, but it cannot stand in for a tenant-branded compliance workflow.
References
The Mustache syntax manual defines escaped and unescaped interpolation. The FTC CAN-SPAM business guide describes US commercial-message obligations; it does not classify every possible notice for your application.
Top comments (0)