emailr_
Todos los artículos
usecase·10 min

Email multi-tenant: Patrones de arquitectura

saasmulti-tenantarchitecture

Las plataformas SaaS multi-tenant necesitan sistemas de email que aíslen a los tenants mientras comparten la infraestructura de forma eficiente. Aquí te mostramos cómo diseñar el email para entornos multi-tenant.

Desafíos del multi-tenant

  • Aislamiento por tenant (reputación, datos)
  • Dominios de envío personalizados por tenant
  • Límites de tasa y cuotas por tenant
  • Plantillas y branding específicos por tenant
  • Facturación y analítica consolidadas

Patrones de arquitectura

Aislamiento por tenant

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


### Envío con contexto de 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
    }
  });
}

Dominios de envío personalizados

Flujo de verificación de dominio

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

Verificación de dominio

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

Limitación de tasa por 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);
  }
}

Gestión de plantillas

Sobrescritura de plantillas por 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);
}

Inyección 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);
}

Aislamiento de reputación

IPs dedicadas por 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();
}

Analítica por 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]);
}

Buenas prácticas

  1. Aislar la reputación - Un tenant problemático no debe afectar a otros
  2. Aplicar límites - Cuotas y límites de tasa por tenant
  3. Soportar dominios personalizados - Apariencia profesional para los tenants
  4. Centralizar las plantillas - Con capacidad de sobrescritura por tenant
  5. Seguimiento por tenant - Analítica, facturación, uso
  6. Planificar para escalar - Diseñar para miles de tenants

El email multi-tenant se trata de equilibrio: infraestructura compartida para la eficiencia, aislamiento para la seguridad y la personalización.

e_

Escrito por el equipo de emailr

Construyendo infraestructura de email para desarrolladores

¿Listo para empezar a enviar?

Obtén tu clave API y envía tu primer email en menos de 5 minutos. No se requiere tarjeta de crédito.