Subdominios multi-tenant en Next.js 14 sin volverte loco
Middleware, DNS comodín y el modelo de datos que mantiene honesto el aislamiento de tenants. Una guía de campo desde producción.
Un subdominio por tenant parece una función de marca y resulta ser una decisión de arquitectura. acme.tuapp.com y globex.tuapp.com deben sentirse como productos separados mientras corren sobre una base de código y un despliegue. Esta es la configuración que sostiene a Forzive en producción.
Primero el DNS comodín
Antes de cualquier código, apunta un registro comodín *.tuapp.com a tu host y provisiona un certificado TLS comodín. Cloudflare hace ambas cosas gratis, y es la pieza que la gente olvida — sin ella, cada tenant nuevo significa una entrada de DNS manual, lo que mata el alta self-serve. Con ella, un subdominio nuevo simplemente funciona en el instante en que un tenant se registra.
Resuelve el tenant en el middleware
El middleware corre antes de cada petición, lo que lo convierte en el único lugar honesto para averiguar quién pregunta. Parsea el host, quita el dominio raíz y pasa la etiqueta restante a la app como un header. Lo clave: la app lee la tenancy de ese header — nunca de la URL — para que el resto de tu código no le importe cómo se resolvió el tenant.
// middleware.ts — resolve the tenant from the host on every request.
import { NextRequest, NextResponse } from "next/server";
const ROOT = "forzive.com";
export function middleware(req: NextRequest) {
const host = req.headers.get("host") ?? "";
const sub = host.replace(`.${ROOT}`, "");
// No subdomain (or "www") → marketing site, untouched.
if (host === ROOT || sub === "www") return NextResponse.next();
// Pass the tenant down as a header — the app never reads it from the URL.
const res = NextResponse.next();
res.headers.set("x-tenant", sub);
return res;
}www.tuapp.com se convierte en un tenant llamado "www" y tu landing da 404.El modelo de datos es donde de verdad ocurre el aislamiento
El subdominio es solo una clave de búsqueda. El aislamiento real vive en la base de datos: cada tabla propiedad de un tenant lleva un tenantId, y cada consulta tiene alcance al tenant resuelto en el middleware. Canalizo todo esto por un único helper de datos para que ninguna consulta individual pueda olvidar el alcance — el aislamiento es estructural, no algo que deba recordar en la llamada número cuatrocientos.
Resuelve el registro del tenant una vez por petición (cachéalo), y de ahí en adelante el resto de tus handlers reciben un contexto de tenant como entrada. Nunca ven una cadena de host, nunca parsean un subdominio. Esa separación es lo que mantiene el sistema comprensible mientras crece.
Trampas que me costaron una tarde cada una
- Desarrollo local.
localhostno tiene subdominios. Usatenant.localhost:3000(los navegadores modernos lo enrutan) o una herramienta comolvh.me, y haz el dominio raíz configurable para que el mismo middleware funcione en dev y prod. - Cookies entre subdominios. Decide con intención: pon el
Domainde la cookie al dominio raíz si los tenants deben compartir sesión, o acótala por subdominio si no deben. El comportamiento por defecto rara vez es el que quieres. - Caché. Cualquier CDN o caché de
fetchdebe incluir el tenant en su clave, o a un gimnasio se le servirá la página de otro. El enfoque basado en header lo hace explícito — añadex-tenanta la clave de caché y listo.
Acierta con el DNS comodín, la resolución en el middleware y la capa de datos con alcance, y los subdominios multi-tenant dejan de dar miedo. Todo el asunto son quizá 50 líneas de código de infraestructura — la disciplina está en mantener la tenancy en esos pocos lugares y en ningún otro.