emailr_
Tous les articles
usecase·10 min

Email multi-tenant : modèles d'architecture

saasmulti-tenantarchitecture

Les plateformes SaaS multi-tenant ont besoin de systèmes d'email qui isolent les tenants tout en partageant l'infrastructure efficacement. Voici comment concevoir l'email pour le multi-tenant.

Défis du multi-tenant

  • Isolation des tenants (réputation, données)
  • Domaines d'envoi personnalisés par tenant
  • Limites de débit et quotas par tenant
  • Modèles et branding spécifiques au tenant
  • Facturation et analytics consolidées

Modèles d'architecture

Isolation des tenants

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;
  };
}


### Envoi avec contexte tenant

```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
    }
  });
}

Domaines d'envoi personnalisés

Flux de vérification de domaine

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;
}

Vérification du domaine

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) };
}

Limitation de débit par tenant

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);
  }
}

Gestion des modèles

Surcharges de modèles par tenant

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);
}

Injection de branding

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);
}

Isolation de la réputation

IP dédiées par 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 par 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]);
}

Bonnes pratiques

  1. Isoler la réputation - Un mauvais tenant ne doit pas impacter les autres
  2. Faire respecter les limites - Quotas et limites de débit par tenant
  3. Prendre en charge les domaines personnalisés - Apparence professionnelle pour les tenants
  4. Centraliser les modèles - Avec possibilité de surcharge par tenant
  5. Suivre par tenant - Analytics, facturation, usage
  6. Prévoir l'échelle - Concevoir pour des milliers de tenants

L'email multi-tenant est une affaire d'équilibre : infrastructure partagée pour l'efficacité, isolation pour la sûreté et la personnalisation.

e_

Écrit par l'équipe emailr

Nous construisons l'infrastructure email pour les développeurs

Prêt à commencer à envoyer ?

Obtenez votre clé API et envoyez votre premier email en moins de 5 minutes. Aucune carte de crédit requise.