emailr_
Alle Artikel
usecase·10 min

Mandantenfähige E-Mail: Architekturmuster

saasmulti-tenantarchitecture

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('&#123;&#123;logo_url&#125;&#125;', branding.logoUrl)
    .replace('&#123;&#123;primary_color&#125;&#125;', branding.primaryColor)
    .replace('&#123;&#123;company_name&#125;&#125;', 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

  1. Reputation isolieren - Schlechte Mandanten sollten andere nicht beeinflussen
  2. Grenzen durchsetzen - Kontingente und Rate Limits pro Mandant
  3. Eigene Domains unterstützen - Professionelles Auftreten für Mandanten
  4. Vorlagen zentralisieren - Mit Override-Möglichkeit pro Mandant
  5. Pro Mandant nachverfolgen - Analysen, Abrechnung, Nutzung
  6. 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.

e_

Geschrieben vom emailr-Team

Wir bauen Email-Infrastruktur für Entwickler

Bereit zum Senden?

Hol dir deinen API-Schlüssel und sende deine erste E-Mail in unter 5 Minuten. Keine Kreditkarte erforderlich.