QAAST Blog / Arquitectura
Volver al sitio
Febrero 2026 12 minutos

Multi-tenancy en Next.js: aprendizajes reales de construir Schedy

Cometimos errores reales construyendo un SaaS multitenant desde cero. Este artículo resume qué pasó, por qué pasó y cómo lo corregimos en producción.

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.

Estrategia
Ventaja
Cuándo usarla
Subdominio por tenant
Aislamiento claro de contexto
Muchos tenants, dominios propios
Path-based routing
Implementación más simple
Pocos tenants y mismo dominio
DB por tenant
Máximo aislamiento
Regulación estricta
Schema por tenant
Balance costo/aislamiento
SaaS B2B intermedio
RLS por tenant
Una sola DB, costo eficiente
Alto volumen de tenants

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í.

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

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