Multi-tenant SaaS platforms need email systems that isolate tenants while sharing infrastructure efficiently. Here's how to design email for multi-tenancy.
Multi-tenant challenges
- —Tenant isolation (reputation, data)
- —Custom sending domains per tenant
- —Per-tenant rate limits and quotas
- —Tenant-specific templates and branding
- —Consolidated billing and analytics
Architecture patterns
Tenant isolation
interface TenantEmailConfig {
tenantId: string;
sendingDomain: string;
fromAddress: string;
replyTo?: string;
// Authentication
dkim: {
selector: string;
privateKey: string;
};
// Limits
dailyLimit: number;
rateLimit: number; // per second
// Branding
templates: Record<string, string>;
brandColors: {
primary: string;
secondary: string;
};
}
### Sending with tenant context
```typescript
async function sendTenantEmail(tenantId: string, email: Email) {
const config = await getTenantEmailConfig(tenantId);
return sendEmail({
...email,
from: config.fromAddress,
replyTo: config.replyTo,
headers: {
'X-Tenant-ID': tenantId
}
});
}
Custom sending domains
Domain verification flow
async function setupTenantDomain(tenantId: string, domain: string) {
// Generate DNS records
const records = {
spf: {
type: 'TXT',
name: domain,
value: 'v=spf1 include:spf.emailr.dev ~all'
},
dkim: {
type: 'TXT',
name: `emailr._domainkey.${domain}`,
value: await generateDKIMRecord(tenantId)
},
dmarc: {
type: 'TXT',
name: `_dmarc.${domain}`,
value: 'v=DMARC1; p=quarantine; rua=mailto:[email protected]'
}
};
await savePendingDomain(tenantId, domain, records);
return records;
}
Domain verification
async function verifyTenantDomain(tenantId: string, domain: string) {
const pending = await getPendingDomain(tenantId, domain);
const results = await Promise.all([
verifyDNSRecord(pending.records.spf),
verifyDNSRecord(pending.records.dkim),
verifyDNSRecord(pending.records.dmarc)
]);
if (results.every(r => r.verified)) {
await activateDomain(tenantId, domain);
return { status: 'verified' };
}
return { status: 'pending', failed: results.filter(r => !r.verified) };
}
Per-tenant rate limiting
class TenantRateLimiter {
async checkLimit(tenantId: string): Promise<boolean> {
const config = await getTenantConfig(tenantId);
const usage = await getTenantUsage(tenantId, 'day');
return usage.sent < config.dailyLimit;
}
async trackSend(tenantId: string, count: number = 1) {
await redis.hincrby(`tenant:${tenantId}:usage`, 'sent', count);
}
}
Template management
Tenant template overrides
async function getTemplate(tenantId: string, templateId: string) {
// Check for tenant override
const tenantTemplate = await db.query(`
SELECT * FROM tenant_templates
WHERE tenant_id = $1 AND template_id = $2
`, [tenantId, templateId]);
if (tenantTemplate) {
return tenantTemplate;
}
// Fall back to default
return getDefaultTemplate(templateId);
}
Branding injection
async function applyTenantBranding(html: string, tenantId: string) {
const branding = await getTenantBranding(tenantId);
return html
.replace('{{logo_url}}', branding.logoUrl)
.replace('{{primary_color}}', branding.primaryColor)
.replace('{{company_name}}', branding.companyName);
}
Reputation isolation
Dedicated IPs per tenant
interface TenantIPConfig {
tenantId: string;
ipType: 'shared' | 'dedicated';
dedicatedIPs?: string[];
warmupStatus?: 'warming' | 'ready';
}
async function getIPForTenant(tenantId: string): Promise<string> {
const config = await getTenantIPConfig(tenantId);
if (config.ipType === 'dedicated' && config.warmupStatus === 'ready') {
return selectIP(config.dedicatedIPs);
}
return getSharedPoolIP();
}
Analytics per tenant
async function getTenantAnalytics(tenantId: string, period: string) {
return db.query(`
SELECT
COUNT(*) as sent,
SUM(CASE WHEN delivered THEN 1 ELSE 0 END) as delivered,
SUM(CASE WHEN opened THEN 1 ELSE 0 END) as opened,
SUM(CASE WHEN clicked THEN 1 ELSE 0 END) as clicked,
SUM(CASE WHEN bounced THEN 1 ELSE 0 END) as bounced
FROM email_events
WHERE tenant_id = $1
AND created_at > NOW() - $2::interval
`, [tenantId, period]);
}
Best practices
- —Isolate reputation - Bad tenant shouldn't affect others
- —Enforce limits - Per-tenant quotas and rate limits
- —Support custom domains - Professional appearance for tenants
- —Centralize templates - With tenant override capability
- —Track per-tenant - Analytics, billing, usage
- —Plan for scale - Design for thousands of tenants
Multi-tenant email is about balance: shared infrastructure for efficiency, isolation for safety and customization.