Schedy es nuestra plataforma de gestión de citas y CRM para negocios de servicios. Cada negocio que usa Schedy es un tenant independiente, con sus propios datos, configuración y subdominio. Eso suena limpio en diagramas, pero en producción exige decisiones de arquitectura muy específicas.
Este artículo no es teoría. Es un resumen de errores que nos costaron tiempo y de prácticas que hoy consideramos obligatorias.
Stack y decisión base
Stack principal: Next.js 14 (App Router), Prisma, PostgreSQL, Vercel y Stripe.
Modelo elegido: subdominio por tenant + row-level security en base de datos.
El corazón: middleware en Next.js
En un esquema por subdominio, el middleware es donde se resuelve el contexto del tenant. Si ahí fallas, todo falla.
// middleware.ts
export function middleware(request: NextRequest) {
const hostname = request.headers.get("host") || "";
const subdomain = hostname.split(".")[0];
if (isStaticPath(request.nextUrl.pathname)) {
return NextResponse.next();
}
const tenantContext = resolveTenant(subdomain);
const response = NextResponse.next();
response.headers.set("x-tenant-id", tenantContext.id);
return response;
}
Regla clave: el tenantId se resuelve server-side y se propaga al backend. No debería depender de estado en cliente.
Los 5 errores más caros que cometimos
1) Tenant ID en cliente (local/session storage)
Con múltiples tabs, el contexto se pisaba y podías terminar viendo datos del tenant equivocado.
Corrección: resolver tenant en middleware y mantener contexto solo del lado servidor.
2) Middleware corriendo en todos los paths
Estábamos resolviendo tenant incluso para assets estáticos. Eso disparó queries innecesarias y degradó rendimiento.
// matcher recomendado
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.png|.*\\.jpg).*)"],
};
3) Caché sin namespace por tenant
Una clave global en caché puede mezclar datos entre tenants. Eso es un bug de seguridad, no un detalle menor.
// MAL
const cacheKey = `resource:${resourceId}`;
// BIEN
const cacheKey = `tenant:${tenantId}:resource:${resourceId}`;
Hoy en Schedy es regla de code review: toda key de caché debe incluir tenantId.
4) Migraciones sin lógica tenant-aware
Una migración con defaults globales nos dejó tenants con datos inconsistentes y corrección manual en producción.
Corrección: ejecutar migraciones validando comportamiento tenant por tenant cuando tocan datos existentes.
5) Logs sin tenantId
Sin contexto de tenant en logs, debuggear incidentes multiusuario se vuelve casi imposible.
logger.error("Payment processing failed", {
tenantId: context.tenantId,
userId: context.userId,
transactionId: transaction.id,
error: error.message,
});
Cómo testear multi-tenancy sin autoengañarse
No basta con probar que cada tenant funciona. También debes probar que no se mezclan entre sí.
- Tests de aislamiento de datos entre tenants.
- Tests de resolución de contexto por subdominio.
- Tests de caché con concurrencia entre tenants.
it("should not return data from another tenant", async () => {
const tenantA = await createTestTenant();
const tenantB = await createTestTenant();
const resource = await createResource({ tenantId: tenantA.id });
const response = await request
.get(`/api/resources/${resource.id}`)
.set("x-tenant-id", tenantB.id);
expect(response.status).toBe(404);
});
Importante: devolver 404 en accesos cross-tenant evita filtrar que el recurso existe. Un 403 puede exponer información innecesaria.
Qué haríamos diferente si arrancáramos hoy
- Definir el modelo de aislamiento antes de escribir la primera línea de código.
- Implementar logging estructurado con tenantId desde el día uno.
- Diseñar tests de aislamiento antes de tener volumen real de tenants.
- Tener un tenant de QA dedicado para validar producción continuamente.
- Documentar explícitamente cómo se propaga el tenant context en todo el sistema.
Conclusión
Multi-tenancy no es una feature que agregas al final. Es una decisión estructural que impacta routing, datos, caché, logging, testing y cultura de revisión técnica.
Next.js te da buenas primitivas, pero no reemplaza decisiones de arquitectura. Esa parte sigue siendo responsabilidad del equipo.
Si estás construyendo un SaaS multitenant, en QAAST revisamos riesgos de aislamiento, estrategia de QA y arquitectura de datos en una sesión de diagnóstico gratuita.
Agendar diagnóstico gratuito