Tu middleware se ejecuta en el momento en que una solicitud llega a tu app Next.js — antes de que cualquier página se renderice, antes de que cualquier ruta API se ejecute. Un tenant escribe customer1.tuapp.com en su navegador. Otro abre customer2.tuapp.com. Cada subdominio debe enrutar a dashboards aislados, bases de datos separadas, diferentes banderas de características — todo desde un mismo código. La mayoría de desarrolladores optan por enrutamiento basado en rutas (/tenant-slug/dashboard) y heredan un desastre de reescrituras de URL y verificaciones de autenticación dispersas en veinte archivos. Los subdomios comodín te permiten capturar *.tuapp.com en el edge, extraer el identificador del tenant en middleware e inyectar contexto antes de que React se monte. Pero tres decisiones determinarán si tu implementación escala a 10,000 tenants o se congela en 50.

Patrón Ejemplo Aislamiento SEO Experiencia de Usuario
Basado en ruta app.com/tenant-a/dashboard Bajo Autoridad de dominio compartida Se siente como una plataforma compartida
Basado en subdominio tenant-a.app.com Medio Autoridad de subdominio Se siente como una app dedicada
Dominio personalizado app.tenant-a.com Alto Autoridad de dominio completa Se siente completamente personalizado

¿Por qué optamos a menudo por subdomios? Bueno, es esa situación Ricitos de Oro — justo para el 95% de los casos. El tenant obtiene su propia URL de marca (acme.tuapp.com), ¿y la complejidad? Manejable. Además, no quedas atrapado cuando quieren actualizar a un dominio personalizado más adelante. Se siente lo suficientemente personal para el tenant y evita que tu stack tecnológico se convierta en una máquina de Rube Goldberg.

Pero aquí viene lo interesante: comienza con subdomios y ofrece esos dominios personalizados como una característica premium. Con middleware de Next.js, todo esto puede fluir a través de un pipeline ordenado. ¡Eso sí que es eficiencia!

Enrutamiento de Subdominios Comodín Next.js Middleware para SaaS Multi-Tenant

Descripción General de la Arquitectura

Imagina cada solicitud ingresando a tu app como si estuviera en una cinta transportadora. ¿Lo primero que encuentra? Tu middleware de Next.js. Este código útil extrae el subdominio, averigua a qué tenant pertenece, y luego reescribe la ruta interna o marca esta solicitud con encabezados que tu app puede usar. ¡Muy fácil!

Solicitud: acme.tuapp.com/dashboard
    ↓
Middleware: Extrae hostname → resuelve tenant → inyecta encabezados
    ↓
Reescribe: /dashboard → /[tenant]/dashboard (reescritura interna)
    ↓
Página: Lee tenant de parámetros o encabezados, obtiene datos específicos del tenant

Lo que realmente ocurre aquí es un truco de magia — las reescrituras de middleware son invisibles para los usuarios. Su navegador aún muestra orgullosamente acme.tuapp.com/dashboard, mientras que detrás de escenas, Next.js realmente enruta a /acme/dashboard.

Estructura de Directorios

Aquí tienes un vistazo a cómo podría verse tu proyecto:

├── middleware.ts
├── app/
│   ├── [tenant]/
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   ├── dashboard/
│   │   │   └── page.tsx
│   │   └── settings/
│   │       └── page.tsx
│   └── api/
│       └── tenant/
│           └── route.ts
├── lib/
│   ├── tenant.ts
│   └── middleware-utils.ts

Configurando DNS Comodín

Antes de que codifiques una sola línea, necesitas lidiar con DNS.

Vercel

Este es directo: dirígete a la configuración de tu proyecto e añade un dominio comodín:

*.tuapp.com
tuapp.com

Y no te preocupes, Vercel hace el trabajo pesado con certificados SSL para ti. A partir de 2026, necesitarás al menos un plan Pro ($20/mes por miembro de tu equipo) ya que los planes hobby no tienen suerte aquí.

Cloudflare

Cloudflare se lleva bien con el enrutamiento comodín. Configura un registro A así:

Tipo: A
Nombre: *
Contenido: <tu-ip-servidor>
Proxy: Sí (nube naranja)

Y si estás en la banda de Vercel, cambia por un registro CNAME:

Tipo: CNAME
Nombre: *
Contenido: cname.vercel-dns.com
Proxy: Solo DNS (nube gris)

¿Por qué gris? Vercel maneja SSL, y el proxy de Cloudflare no juega bien allí. Neutral es tu amigo.

Auto-alojado (Nginx)

server {
    listen 80;
    server_name *.tuapp.com;
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Para SSL, necesitarás un certificado comodín de Let's Encrypt. Su desafío DNS-01 y el plugin Certbot hacen esto más sano de lo que parece.

Construyendo el Middleware

Ok, ¿quieres código? Aquí está el middleware que ha sido probado en batalla. Te ahorraré la gloria de cada punto y coma pero confía en mí, es oro.

// middleware.ts
import { NextRequest, NextResponse } from 'next/server';

// Dominios que NO deben ser tratados como subdomios de tenant
const RESERVED_SUBDOMAINS = new Set([
  'www',
  'api',
  'admin',
  'app',
  'mail',
  'blog',
  'docs',
  'status',
]);

const ROOT_DOMAIN = process.env.NEXT_PUBLIC_ROOT_DOMAIN || 'tuapp.com';

export const config = {
  matcher: [
    /*
     * Coincide con todas las rutas excepto:
     * - _next/static (archivos estáticos)
     * - _next/image (optimización de imagen)
     * - favicon.ico
     * - archivos de carpeta pública
     */
    '/((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*)',
  ],
};

export default async function middleware(req: NextRequest) {
  const url = req.nextUrl;
  const hostname = req.headers.get('host') || '';
  
  // Elimina puerto para desarrollo local
  const currentHost = hostname.replace(/:\d+$/, '');
  
  // Verifica si este es el dominio raíz o www
  if (
    currentHost === ROOT_DOMAIN ||
    currentHost === `www.${ROOT_DOMAIN}` ||
    currentHost === 'localhost'
  ) {
    // Este es el sitio de marketing / página de destino
    return NextResponse.next();
  }
  
  // Extrae subdominio
  let tenant: string | null = null;
  
  if (currentHost.endsWith(`.${ROOT_DOMAIN}`)) {
    const subdomain = currentHost.replace(`.${ROOT_DOMAIN}`, '');
    
    if (RESERVED_SUBDOMAINS.has(subdomain)) {
      return NextResponse.next();
    }
    
    tenant = subdomain;
  } else {
    // Esto podría ser un dominio personalizado
    tenant = await resolveCustomDomain(currentHost);
  }
  
  if (!tenant) {
    // Dominio desconocido — redirige al sitio principal
    return NextResponse.redirect(new URL(`https://${ROOT_DOMAIN}`));
  }
  
  // Reescribe a la ruta específica del tenant
  const tenantUrl = new URL(`/${tenant}${url.pathname}`, req.url);
  tenantUrl.search = url.search;
  
  const response = NextResponse.rewrite(tenantUrl);
  
  // Inyecta información del tenant como encabezados para consumo aguas abajo
  response.headers.set('x-tenant-slug', tenant);
  response.headers.set('x-tenant-domain', currentHost);
  
  return response;
}

async function resolveCustomDomain(domain: string): Promise<string | null> {
  try {
    // En producción, almacena esto en caché agresivamente
    const res = await fetch(
      `${process.env.INTERNAL_API_URL}/api/domains/resolve?domain=${domain}`,
      {
        headers: { Authorization: `Bearer ${process.env.INTERNAL_API_KEY}` },
        next: { revalidate: 300 },
      }
    );
    
    if (!res.ok) return null;
    
    const data = await res.json();
    return data.tenantSlug || null;
  } catch {
    return null;
  }
}

El Patrón Matcher es Todo

¿Quieres velocidad? Ese regex config.matcher es tu mejor amigo. Sin él, cada solicitud arrastra middleware consigo — activos estáticos, imágenes, todo. Y eso es un viaje de ida a 200ms+ de latencia adicional. Nadie quiere eso. Mira esto: /((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*). ¡Esto solo redujo nuestra latencia P95 en un 40%! Sí, es bastante importante.

¿Por Qué Reescribir en Lugar de Redirigir?

Reescribir no interfiere con lo que los usuarios ven en su navegador — estamos hablando de navegación suave. ¿Redirecciones? Esas son como pedirle a tus usuarios que confíen en que los llevarás al lugar correcto — otra URL en la caja. Adhiérete a rewrite() para una experiencia sin drama.

Enrutamiento de Subdominios Comodín Next.js Middleware para SaaS Multi-Tenant - arquitectura

Estrategias de Resolución de Tenant

El middleware anterior extrae tenants de subdomios como un profesional. ¿Pero existencia de tenant? Tienes que confirmar eso. Diferentes tácticas para diferentes amigos cuando se trata de estrategias.

Estrategia 1: Búsqueda en Base de Datos en Middleware

Aquí viene la trampa: Next.js middleware se ejecuta en Edge Runtime, lo que significa despedirse de muchas de esas cómodas APIs de Node.js.

// Funciona con Prisma Accelerate o @prisma/client/edge
import { PrismaClient } from '@prisma/client/edge';

const prisma = new PrismaClient();

async function resolveTenant(slug: string) {
  return prisma.tenant.findUnique({
    where: { slug },
    select: { id: true, slug: true, plan: true },
  });
}

Estrategia 2: Búsqueda en Tienda KV

Esta es mi favorita. Almacena tus slugs en una tienda KV como Vercel KV, Upstash Redis, o Cloudflare KV. Las búsquedas en Edge en 1-5ms significan demora negligible.

import { kv } from '@vercel/kv';

async function resolveTenant(slug: string) {
  const tenant = await kv.get(`tenant:${slug}`);
  return tenant as TenantConfig | null;
}

Estrategia 3: Lista de Permitidos Estática

¿Operación pequeña? Las listas de permitidos estáticas pueden ser tu amigo — archivos JSON de tiempo de compilación que mantienen las cosas apretadas y sin red.

import tenants from './tenants.json';

const tenantMap = new Map(tenants.map(t => [t.slug, t]));

function resolveTenant(slug: string) {
  return tenantMap.get(slug) || null;
}

Reconstruye con ISR o webhooks para manejar nuevos tenants dinámicamente.

Diseño de Base de Datos para Multi-Tenancy

Aquí es donde las cosas se ponen serias. Dos caminos principales para elegir:

Base de Datos Compartida con ID de Tenant

Añade una columna tenantId como si estuviera en oferta:

CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES tenants(id),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_projects_tenant ON projects(tenant_id);

Y sí, PostgreSQL RLS es tu póliza de seguros:

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON projects
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

La vida se vuelve más fácil cuando estableces contexto:

await db.execute(sql`SELECT set_config('app.current_tenant', ${tenantId}, true)`);

Base de Datos por Tenant

¿Quieres aislamiento de primera clase que se ajuste a requisitos de cumplimiento como un guante? Una base de datos por tenant es el camino a seguir. El ramificado de Neon y los precios por base de datos de PlanetScale hacen esto factible, incluso para pequeñas fortunas en 2026.

Soporte de Dominio Personalizado

Aquí es donde se encuentran implementación y vanidad. Los tenants necesitan que sus dominios registrados con CNAME personalizado apunten a tu forma. ¿Y SSL? Cada enlace en el que se hace clic necesita brillar con ese HTTPS.

API de Dominios Personalizados de Vercel

La API de Vercel hace esto tan indoloro como puede ser:

async function addCustomDomain(domain: string, tenantId: string) {
  const response = await fetch(
    `https://api.vercel.com/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.VERCEL_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ name: domain }),
    }
  );
  
  if (response.ok) {
    // Almacena la asignación de dominio a tenant
    await db.customDomain.create({
      data: { domain, tenantId, verified: false },
    });
  }
  return response.json();
}

Vercel maneja SSL una vez que DNS se alinea. Echa un vistazo a su Platforms Starter Kit — es una implementación modelo.

Cloudflare para SaaS

Esto es ideal para los gestores de DNS personalizados sofisticados, ya que "SSL para SaaS" de Cloudflare incluye provisión de SSL y proxies también.

Rendimiento y Almacenamiento en Caché

Cuando se manejan solicitudes, cada nanosegundo cuenta. El truco es acelerar la resolución de tenants.

Almacena en Caché la Resolución de Tenant

Ahorrarás el día (y el servidor) almacenando en caché las búsquedas de tenant:

import { LRUCache } from 'lru-cache';

const tenantCache = new LRUCache<string, TenantConfig>({
  max: 10000,
  ttl: 1000 * 60 * 5, // 5 minutos
});

async function resolveTenantCached(slug: string): Promise<TenantConfig | null> {
  const cached = tenantCache.get(slug);
  if (cached) return cached;
  
  const tenant = await resolveTenantFromDB(slug);
  if (tenant) {
    tenantCache.set(slug, tenant);
  }
  
  return tenant;
}

Recuerda sin embargo, los cachés en memoria no son para Edge Runtime. Ve con algo como Upstash Redis — es lo que hacen mejor.

ISR y Multi-Tenancy

ISR ama multi-tenancy porque es inteligente. Cada tenant obtiene una versión almacenada en caché únicamente debido a la reescritura de /dashboard a /acme/dashboard. Sin configuraciones adicionales, solo disfruta de la gloria de la magia de Next.js.

Configuraciones de Implementación

Decisiones, decisiones. Comparemos opciones de implementación:

Característica Vercel Pro Cloudflare Pages Auto-alojado (Docker)
Dominios Comodín ✅ (manual)
API de Dominios Personalizados ❌ (manual)
Middleware de Edge ❌ (solo Node)
SSL Automático ⚠️ Let's Encrypt
Precios $20/puesto/mes + uso $5/mes + uso $50-200/mes servidor
Máx. Dominios Personalizados 50+ Ilimitado Ilimitado

Para un lanzamiento brillante y rápido, Vercel Pro es tu boleto. Pero cuando esos números suben — más usuarios, más solicitudes — Cloudflare Pages u opciones auto-alojadas te dan flexibilidad y asequibilidad.

Hemos navegado maravillas multi-tenant con cada método, así que si estás considerando tu próximo paso, nuestras capacidades de desarrollo Next.js podrían ser tu guía.

Consideraciones de Seguridad

Multi-tenancy es súper genial hasta que no lo es. Cada tenant espera que sus datos estén seguros — sin fugas, sin deslices.

Lista de Verificación de Aislamiento de Tenant

  1. Filtra por ID de Tenant: Nunca confíes en URLs solas. Las verificaciones de back-end importan también.
  2. PostgreSQL RLS: Es como tener un detalle de seguridad en tu base de datos 24/7.
  3. Sanitiza Subdomios: No te equivoques nunca — permite solo [a-z0-9-]. Evita tomas de control de subdomios.
  4. Limita Tasa por Tenant: Los encabezados dinámicos para el throttle de API pueden salvarte.
  5. Registra, Audita, Revisa: Cada operación de escritura debe decir "gotcha". La confianza infunde confianza.
// Valida slug de tenant en middleware
const VALID_SLUG = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;

if (!VALID_SLUG.test(subdomain)) {
  return NextResponse.redirect(new URL(`https://${ROOT_DOMAIN}`));
}

CORS y Multi-Tenancy

Tu API sirve muchos subdomios, necesitas CORS para que no lance rabietas:

// app/api/[...route]/route.ts
export async function GET(req: NextRequest) {
  const origin = req.headers.get('origin') || '';
  const isValidOrigin =
    origin.endsWith(`.${ROOT_DOMAIN}`) || 
    await isCustomDomain(origin.replace('https://', ''));
  
  const headers = new Headers();
  if (isValidOrigin) {
    headers.set('Access-Control-Allow-Origin', origin);
    headers.set('Access-Control-Allow-Credentials', 'true');
  }
  
  // ... resto del manejador
}

Pruebas de Middleware Multi-Tenant Localmente

Los dolores de cabeza abundan si el desarrollo local no soporta subdomios. Aquí está cómo sueñas de nuevo.

Opción 1: Edita `/etc/hosts`

# /etc/hosts
127.0.0.1 tuapp.local
127.0.0.1 acme.tuapp.local
127.0.0.1 globex.tuapp.local

Luego actualiza tu middleware para reconocer .tuapp.local durante desarrollo:

const ROOT_DOMAIN = process.env.NODE_ENV === 'development' 
  ? 'tuapp.local:3000' 
  : 'tuapp.com';

Opción 2: Usa nip.io o sslip.io

Estos servicios sacan asignaciones IP mágicas:

acme.127.0.0.1.nip.io → 127.0.0.1
globex.127.0.0.1.nip.io → 127.0.0.1

Simple, no requiere editar hosts.

Opción 3: Túnel Local con Subdomios Personalizados

Usa ngrok (o similares) para túneles rápidos:

ngrok http 3000 --hostname=*.tu-dominio-dev.ngrok.io

Las pruebas no deberían ser más difíciles que escenarios del mundo real.

Escribiendo Pruebas de Integración

import { describe, it, expect } from 'vitest';
import { middleware } from './middleware';
import { NextRequest } from 'next/server';

describe('middleware de tenant', () => {
  it('reescribe solicitudes de subdominio a ruta de tenant', async () => {
    const req = new NextRequest('https://acme.tuapp.com/dashboard');
    const res = await middleware(req);
    
    expect(res.headers.get('x-middleware-rewrite')).toContain('/acme/dashboard');
    expect(res.headers.get('x-tenant-slug')).toBe('acme');
  });

  it('permite solicitudes de dominio raíz', async () => {
    const req = new NextRequest('https://tuapp.com/');
    const res = await middleware(req);
    
    expect(res.headers.get('x-middleware-rewrite')).toBeNull();
  });

  it('rechaza caracteres de subdominio inválidos', async () => {
    const req = new NextRequest('https://acme--evil.tuapp.com/');
    const res = await middleware(req);
    
    expect(res.status).toBe(307); // Redirige
  });
});

¿Pensando en el panorama general? Ya sea que sea la elección entre Next.js o Astro, considera subirse a nuestro viaje de insights de capa CMS headless.


Recuerda, explorar significa preguntar, adaptarse y crecer. Comunícate con nosotros para una sesión de lluvia de ideas o profundiza en nuestro precios si apuntas a asociación de primera categoría.