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('{{logo_url}}', branding.logoUrl)
.replace('{{primary_color}}', branding.primaryColor)
.replace('{{company_name}}', 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
- —Aislar la reputación - Un tenant problemático no debe afectar a otros
- —Aplicar límites - Cuotas y límites de tasa por tenant
- —Soportar dominios personalizados - Apariencia profesional para los tenants
- —Centralizar las plantillas - Con capacidad de sobrescritura por tenant
- —Seguimiento por tenant - Analítica, facturación, uso
- —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.