Next.js SaaS
Cómo diseñar un SaaS multitenant en Next.js: aislamiento, enrutamiento y propiedad de los datos
Diseña la resolución de tenants, la propiedad de los datos, la autorización, las claves de caché, los trabajos, los dominios personalizados y el despliegue de un SaaS multitenant en Next.js.
La separación entre tenants es una condición que siempre debe cumplirse: cada lectura, escritura, entrada de caché, trabajo, registro y nombre de host debe corresponder al tenant previsto antes de iniciar una operación con privilegios.
Define qué significa un tenant
Un tenant puede ser una organización, un espacio de trabajo, una tienda o una cuenta de cliente. Define quién lo crea, quién pertenece a él, si los usuarios pueden pertenecer a varios y qué registros le corresponden. Usa internamente un ID de tenant estable, aunque el identificador público sea un slug o un nombre de host personalizado.
Escribe el compromiso de aislamiento. Una infraestructura compartida con propiedad de los datos a nivel de fila es distinta de una base de datos por tenant. El modelo más estricto cuesta más de aprovisionar y operar, pero puede estar justificado por la normativa, la escala o los requisitos de los clientes. Elige según los límites reales del producto, en lugar de buscar una pureza abstracta.
Resuelve el contexto del tenant a partir de datos de confianza
Las aplicaciones basadas en rutas pueden resolver /acme/projects a partir del slug de la ruta. Las aplicaciones con subdominios y dominios personalizados resuelven el host de la solicitud para obtener el registro del tenant. Normaliza el host, rechaza los valores desconocidos y nunca confíes en un ID de tenant enviado por un formulario cuando la ruta autenticada ya establece el contexto.
Proxy puede realizar reescrituras tempranas del nombre de host o enrutamiento optimista, pero la resolución segura también debe estar en el código del servidor. Pasa un contexto de tenant verificado a la capa de datos; no dejes que cada componente analice las cabeceras por su cuenta y deduzca la propiedad de los datos.
export async function resolveTenant(host: string) {
const normalized = host.toLowerCase().split(":")[0];
const tenant = await db.tenant.findUnique({
where: { hostname: normalized },
});
if (!tenant) notFound();
return tenant;
}Exige la propiedad de los datos en cada consulta
Añade tenantId a los registros de tablas compartidas e inclúyelo en cada consulta, actualización, eliminación y restricción de unicidad. Un ID de registro único globalmente no es una comprobación de autorización. Consulta tanto por el ID solicitado como por el ID de tenant verificado para que un identificador expuesto no pueda cruzar ese límite.
La seguridad a nivel de fila de la base de datos puede aportar defensa en profundidad cuando la arquitectura la admite, pero la autorización de la aplicación sigue siendo necesaria. Prueba deliberadamente los casos de denegación creando dos tenants e intentando cada operación protegida con los identificadores del otro tenant.
const project = await db.project.findFirst({
where: {
id: projectId,
tenantId: context.tenantId,
},
});Define roles específicos para cada tenant
Un usuario puede ser propietario en un tenant y lector en otro. Guarda los roles en los registros de pertenencia, en lugar de usar un único campo global en el usuario. Resuelve la pertenencia después del tenant y comprueba el permiso exacto que requiere la operación.
Evita dispersar cadenas de roles por los componentes. Centraliza las reglas de permisos en funciones que devuelvan decisiones de dominio y repite la comprobación dentro de Server Actions y Route Handlers. La interfaz puede ocultar los controles no disponibles para mayor claridad, pero la autorización del servidor es lo que protege la operación.
Separa las cachés, los trabajos y el almacenamiento
Cada clave de caché compartida necesita la identidad del tenant. Una clave llamada projects puede exponer el resultado de un tenant a otro; projects:tenant-id establece el límite. Aplica la misma regla a las etiquetas de caché, los límites de solicitudes, las rutas de almacenamiento de objetos, los índices de búsqueda, los datos de los trabajos y las claves de idempotencia.
Los workers en segundo plano deben volver a resolver la autorización o actuar a partir de un contexto de tenant inmutable y de confianza, registrado al crear el trabajo. Incluye en los registros identificadores seguros para cada tenant, pero no registres contenido privado de los clientes solo para facilitar la depuración.
"use cache";
cacheTag("projects:" + tenantId);
return db.project.findMany({ where: { tenantId } });Trata los dominios personalizados como configuración verificada
Un tenant no debe poder reclamar un nombre de host con solo escribirlo en un formulario. Exige un paso de verificación antes de enrutar el tráfico, impide que un nombre de host pertenezca a dos tenants y define qué sucede cuando cambia su propietario. Mantén los dominios propios de la plataforma separados de los dominios gestionados por los clientes.
Genera las URL canónicas a partir del nombre de host verificado del tenant cuando sus páginas públicas sean indexables. Los paneles que requieren autenticación normalmente no deberían indexarse. Haz que las redirecciones y las cookies sean compatibles con el modelo de nombres de host, especialmente cuando los usuarios pasen de un dominio central de inicio de sesión al dominio de un tenant.
Prueba el aislamiento como propiedad del sistema
Crea datos de prueba automatizados para al menos dos tenants y dos roles. Prueba el acceso directo a páginas, Server Actions, Route Handlers, archivos exportados, cachés, trabajos, búsquedas y resolución de hosts. Una prueba que solo oculta el elemento de navegación de otro tenant no comprueba el aislamiento.
Revisa las consultas y los registros de la base de datos para detectar condiciones de tenant ausentes. Añade restricciones de unicidad que incluyan tenantId cuando los nombres solo deban ser únicos dentro de un tenant. Prueba la eliminación y la exportación de tenants para que los datos en segundo plano no queden accidentalmente sin propietario.
Despliega el enrutamiento de tenants con versiones observables
Adios mantiene el enrutamiento de Next.js, el entorno de ejecución persistente, la dependencia de base de datos, las referencias a secretos, los registros y la configuración de dominios personalizados asociados a la versión de la aplicación. En la vista previa, prueba un nombre de host de la plataforma, un nombre de host verificado de un tenant, un host desconocido, dos cuentas de tenant y la ruta de comprobación de estado antes de promover la versión.
Los registros de compilación y del entorno de ejecución ayudan a distinguir una versión fallida de un error de datos específico de un tenant. La gestión de TLS y la promoción mantienen las rutas públicas verificadas en la versión que funciona correctamente, mientras que los cambios en el código fuente y el manifiesto se pueden revisar. El aislamiento entre tenants sigue siendo responsabilidad del diseño de la aplicación y de los datos; la capa de alojamiento permite desplegar e inspeccionar ese diseño.
name: multi-tenant-app
build_cmd: npm ci && npm run build
start_cmd: npm start
runtime:
name: node@24
port: 3000
health_path: /api/health
env:
DATABASE_URL: secret://DATABASE_URL