Mandantenfähige SaaS-Plattformen brauchen E-Mail-Systeme, die Mandanten isolieren und gleichzeitig Infrastruktur effizient teilen. So entwirfst du E-Mail für Multi-Tenancy.
Herausforderungen bei Mandantenfähigkeit
- —Mandantenisolation (Reputation, Daten)
- —Eigene Versanddomains pro Mandant
- —Rate Limits und Kontingente pro Mandant
- —Mandantenspezifische Vorlagen und Branding
- —Konsolidierte Abrechnung und Analysen
Architekturmuster
Mandantenisolation
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;
};
}
### Versand mit Mandantenkontext
```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
}
});
}
Eigene Versanddomains
Ablauf der Domain-Verifizierung
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-Verifizierung
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) };
}
Rate Limiting pro Mandant
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);
}
}
Vorlagenverwaltung
Vorlagen-Overrides pro Mandant
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-Integration
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);
}
Reputationsisolation
Dedizierte IPs pro Mandant
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();
}
Analysen pro Mandant
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
- —Reputation isolieren - Schlechte Mandanten sollten andere nicht beeinflussen
- —Grenzen durchsetzen - Kontingente und Rate Limits pro Mandant
- —Eigene Domains unterstützen - Professionelles Auftreten für Mandanten
- —Vorlagen zentralisieren - Mit Override-Möglichkeit pro Mandant
- —Pro Mandant nachverfolgen - Analysen, Abrechnung, Nutzung
- —Auf Skalierung planen - Für Tausende von Mandanten auslegen
Mandantenfähige E-Mail dreht sich um Balance: geteilte Infrastruktur für Effizienz, Isolation für Sicherheit und Anpassung.