Multi-tenancy: la decisión arquitectónica central de todo SaaS
Multi-tenancy es cómo tu sistema sirve a múltiples clientes (tenants) desde la misma infraestructura. Los tres modelos principales tienen tradeoffs muy distintos en costo, seguridad y complejidad operacional.
Silo Model: infraestructura dedicada por tenant
Cada tenant tiene su propia instancia de la aplicación y su propia base de datos. El aislamiento es total: un bug o breach en el tenant A no afecta al tenant B. El costo operacional es alto — cada tenant nuevo requiere provisionar infraestructura. Modelo correcto para clientes enterprise con requerimientos de compliance (datos en su propia VPC, control total de la base de datos).
Pool Model: base de datos compartida con tenant_id
Todos los tenants comparten la misma base de datos. El aislamiento se gestiona a nivel de aplicación con un tenant_id en cada tabla. El costo operacional es bajo — onboardear un tenant nuevo es insertar un registro. El riesgo: un bug de tenancy leak (query que olvida el filtro por tenant_id) puede exponer datos de un tenant a otro.
Bridge Model: schema por tenant en la misma instancia de base de datos
Cada tenant tiene su propio schema dentro de la misma instancia de PostgreSQL (o database en la misma instancia de MySQL). Mejor aislamiento que Pool sin el costo de instancias separadas. La complejidad: gestionar migraciones de schema a través de todos los tenants activos.
// Middleware de multi-tenancy con tenant_id en Pool Model
// El tenant_id se propaga en el contexto de cada request
import { AsyncLocalStorage } from 'node:async_hooks';
const tenantStorage = new AsyncLocalStorage<string>();
// Middleware: extraer tenant del JWT y establecer el contexto
export const tenantMiddleware = async (req: Request, res: Response, next: NextFunction) => {
const tenantId = req.auth?.tenantId;
if (!tenantId) return res.status(401).json({ error: 'Tenant no identificado' });
tenantStorage.run(tenantId, () => next());
};
// Prisma extension: añadir tenant_id automáticamente a todas las queries
export const tenantPrismaExtension = Prisma.defineExtension({
query: {
$allModels: {
async findMany({ args, query }) {
const tenantId = tenantStorage.getStore();
if (!tenantId) throw new Error('Tenant context no establecido');
args.where = { ...args.where, tenant_id: tenantId };
return query(args);
},
async create({ args, query }) {
const tenantId = tenantStorage.getStore();
if (!tenantId) throw new Error('Tenant context no establecido');
args.data = { ...args.data, tenant_id: tenantId };
return query(args);
},
},
},
});Billing por uso: el engine financiero del SaaS
El billing de SaaS tiene tres modelos principales:
- Flat rate: precio fijo mensual. Simple de implementar, predecible para el cliente, pero puede penalizar a los clientes que usan poco y subsidia a los que usan mucho.
- Per seat: precio por usuario activo. El más común en SaaS B2B. Incentivo natural para que el cliente tenga solo los usuarios que necesita.
- Usage-based (consumo): precio por unidad de uso (API calls, GB almacenados, transacciones procesadas). Menor barrera de entrada, pero ingresos impredecibles. Tendencia creciente en SaaS de infraestructura y AI.
// Registro de uso para billing por consumo con Stripe Metered Billing
async function recordApiUsage(tenantId: string, count: number): Promise<void> {
const subscription = await db.subscription.findUnique({
where: { tenantId },
select: { stripeSubscriptionItemId: true }
});
if (!subscription?.stripeSubscriptionItemId) return;
// Stripe acumula el uso y lo factura al final del período
await stripe.subscriptionItems.createUsageRecord(
subscription.stripeSubscriptionItemId,
{
quantity: count,
timestamp: Math.floor(Date.now() / 1000),
action: 'increment', // acumular, no sobrescribir
}
);
}
// Llamar después de cada operación facturable
await recordApiUsage(tenantId, requestCount);Onboarding automatizado: el flujo que determina la retención
En SaaS B2B, el tiempo-a-valor (time-to-value) es el metric más correlacionado con retención. Un cliente que llega al primer valor concreto en menos de 24 horas tiene 3x más probabilidad de convertir de trial a pago que uno que tarda una semana. El onboarding técnico debe ser completamente automatizado.
// Pipeline de onboarding de tenant nuevo — completamente automatizado
async function onboardNewTenant(signupData: SignupDto): Promise<Tenant> {
return db.transaction(async (trx) => {
// 1. Crear tenant y usuario admin
const tenant = await Tenant.create({
name: signupData.companyName,
plan: 'trial',
trialEndsAt: addDays(new Date(), 14),
}, { transaction: trx });
const adminUser = await User.create({
tenantId: tenant.id,
email: signupData.email,
role: 'admin',
}, { transaction: trx });
// 2. Provisionar datos de ejemplo para reducir tiempo-a-valor
await seedDemoData(tenant.id, trx);
// 3. Crear customer en Stripe para futura conversión
const stripeCustomer = await stripe.customers.create({
email: signupData.email,
name: signupData.companyName,
metadata: { tenantId: tenant.id },
});
await tenant.update({ stripeCustomerId: stripeCustomer.id }, { transaction: trx });
// 4. Enviar email de bienvenida con instrucciones
await emailQueue.add('welcome', {
to: signupData.email,
tenantId: tenant.id,
adminUserId: adminUser.id,
});
return tenant;
});
}Feature flags y planes: el control de acceso del SaaS
En un SaaS con múltiples planes (Starter, Professional, Enterprise), el control de acceso a features no debe estar hardcodeado como condiciones en el código — eso se convierte en una pesadilla de mantenimiento. El patrón correcto: un sistema de entitlements que mapea plan → features disponibles.
// Sistema de entitlements para planes SaaS
const PLAN_FEATURES = {
starter: ['api_access', 'basic_reports', 'email_support'],
professional: ['api_access', 'advanced_reports', 'webhooks', 'priority_support', 'sso'],
enterprise: ['api_access', 'advanced_reports', 'webhooks', 'priority_support', 'sso',
'custom_domain', 'audit_logs', 'dedicated_support', 'sla_guarantee'],
} as const;
async function hasFeature(tenantId: string, feature: string): Promise<boolean> {
const tenant = await getTenantWithPlan(tenantId); // cached
const planFeatures = PLAN_FEATURES[tenant.plan as keyof typeof PLAN_FEATURES] ?? [];
return (planFeatures as string[]).includes(feature);
}
// En el handler de API:
if (!await hasFeature(tenantId, 'webhooks')) {
return res.status(402).json({
error: 'Esta función requiere el plan Professional o superior',
upgradeUrl: '/settings/billing'
});
}Observabilidad SaaS: métricas de negocio como métricas técnicas
En un SaaS, las métricas de negocio deben ser tan visibles como las métricas de infraestructura. MRR (Monthly Recurring Revenue), churn, trial-to-paid conversion, y daily active tenants son métricas que el equipo de ingeniería debe poder leer en el mismo dashboard que la latencia de API.
- Alertar cuando el número de tenants activos en las últimas 24h baja un 20% vs. el promedio semanal (señal de posible problema en el login o en features críticas)
- Trackear feature adoption por plan (qué % de tenants Professional usa SSO) para informar decisiones de roadmap
- Monitorear tiempo promedio de onboarding completion por cohorte de signup
Preguntas frecuentes
¿Cuál modelo de multi-tenancy debo elegir para un SaaS B2B nuevo?
¿Cómo manejo la seguridad de datos entre tenants en el Pool Model?
¿Stripe o un sistema de billing propio para un SaaS?
¿Cómo diseño la arquitectura para soportar SSO/SAML que requieren los clientes enterprise?
¿Cómo gestiono las migraciones de base de datos en un SaaS con muchos tenants en Bridge Model?
¿Estás construyendo un producto SaaS y quieres tomar las decisiones de arquitectura correctas desde el inicio? Nuestro equipo puede guiarte en el diseño.
Habla con el equipo