Seu middleware é executado no momento em que uma requisição chega ao seu app Next.js — antes de qualquer página ser renderizada, antes de qualquer rota de API ser executada. Um tenant digita customer1.yourapp.com no seu navegador. Outro abre customer2.yourapp.com. Cada subdomínio deve rotear para dashboards isolados, bancos de dados separados, diferentes feature flags — tudo a partir de uma única base de código. A maioria dos desenvolvedores recorre ao roteamento baseado em caminho (/tenant-slug/dashboard) e herda uma bagunça de rewrites de URL e verificações de autenticação espalhadas por vinte arquivos. Subdomínios wildcard permitem que você capture *.yourapp.com na borda, extraia o identificador do tenant no middleware e injete contexto antes do React montar. Mas três decisões determinarão se a sua implementação escala para 10.000 tenants ou quebra em 50.

Padrão Exemplo Isolamento SEO Experiência do Usuário
Baseado em caminho app.com/tenant-a/dashboard Baixo Autoridade de domínio compartilhada Parece uma plataforma compartilhada
Baseado em subdomínio tenant-a.app.com Médio Autoridade de subdomínio Parece um app dedicado
Domínio customizado app.tenant-a.com Alto Autoridade de domínio completo Parece totalmente com white-label

Por que frequentemente chegamos aos subdomínios? Bem, é aquela situação Goldilocks — perfeita para 95% dos casos. O tenant ganha sua própria URL marcada (acme.yourapp.com) e a complexidade? Gerenciável. Além disso, você não fica preso quando ele quer fazer upgrade para um domínio customizado depois. Parece pessoal o suficiente para o tenant e mantém sua tech stack longe de se tornar uma máquina de Rube Goldberg.

Mas aqui está o detalhe: comece com subdomínios e ofereça esses domínios customizados como um recurso premium. Com o middleware Next.js, tudo isso pode fluir através de um pipeline único e elegante. Que eficiência, certo?

Subdomínio Wildcard Next.js Middleware para Multi-Tenant SaaS

Visão Geral da Arquitetura

Imagine cada requisição entrando no seu app como se estivesse em uma esteira transportadora. A primeira coisa que ela encontra? Seu middleware Next.js. Este código confiável extrai o subdomínio, descobre a qual tenant ele pertence e então reescreve o caminho interno ou marca esta requisição com headers que seu app pode usar. Fácil demais!

Requisição: acme.yourapp.com/dashboard
    ↓
Middleware: Extrair hostname → resolver tenant → injetar headers
    ↓
Rewrite: /dashboard → /[tenant]/dashboard (rewrite interno)
    ↓
Página: Lê tenant dos parâmetros ou headers, busca dados específicos do tenant

O que está realmente acontecendo aqui é um truque de mágica — rewrites do middleware são invisíveis para os usuários. Seu navegador ainda exibe com orgulho acme.yourapp.com/dashboard, enquanto por trás das cortinas, Next.js realmente roteia para /acme/dashboard.

Estrutura de Diretório

Aqui está uma espiada no que seu projeto pode parecer:

├── 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 Wildcard

Antes de você codificar uma única linha, você precisa lidar com DNS.

Vercel

Este é direto: vá para as configurações do seu projeto e adicione um domínio wildcard:

*.yourapp.com
yourapp.com

E não se preocupe, Vercel faz o trabalho pesado com certificados SSL para você. A partir de 2026, você precisará de pelo menos um plano Pro ($20/mês por membro da equipe), já que planos hobby não têm sorte aqui.

Cloudflare

Cloudflare se comporta bem com roteamento wildcard. Configure um registro A assim:

Type: A
Name: *
Content: <seu-ip-servidor>
Proxy: Yes (nuvem laranja)

E se você está na turma Vercel, troque por um registro CNAME:

Type: CNAME
Name: *
Content: cname.vercel-dns.com
Proxy: DNS only (nuvem cinza)

Por que cinza? Vercel cuida do SSL, e o proxy do Cloudflare não se comporta bem lá. Neutro é seu amigo.

Auto-Hospedado (Nginx)

server {
    listen 80;
    server_name *.yourapp.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, você precisará de um certificado wildcard do Let's Encrypt. Seu challenge DNS-01 e plugin Certbot tornam isso mais sensato do que parece.

Construindo o Middleware

Okay, você quer código? Aqui está o middleware que é testado em batalha. Vou poupar você da glória de cada ponto e vírgula, mas confie em mim, é ouro.

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

// Domínios que NÃO devem ser tratados como subdomínios de tenant
const RESERVED_SUBDOMAINS = new Set([
  'www',
  'api',
  'admin',
  'app',
  'mail',
  'blog',
  'docs',
  'status',
]);

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

export const config = {
  matcher: [
    /*
     * Corresponde todos os caminhos exceto:
     * - _next/static (arquivos estáticos)
     * - _next/image (otimização de imagem)
     * - favicon.ico
     * - arquivos da pasta public
     */
    '/((?!_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') || '';
  
  // Remove porta para desenvolvimento local
  const currentHost = hostname.replace(/:\d+$/, '');
  
  // Verifica se este é o domínio raiz ou www
  if (
    currentHost === ROOT_DOMAIN ||
    currentHost === `www.${ROOT_DOMAIN}` ||
    currentHost === 'localhost'
  ) {
    // Este é o site de marketing / página de destino
    return NextResponse.next();
  }
  
  // Extrai subdomínio
  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 {
    // Isto pode ser um domínio customizado
    tenant = await resolveCustomDomain(currentHost);
  }
  
  if (!tenant) {
    // Domínio desconhecido — redireciona para site principal
    return NextResponse.redirect(new URL(`https://${ROOT_DOMAIN}`));
  }
  
  // Reescreve para caminho específico do tenant
  const tenantUrl = new URL(`/${tenant}${url.pathname}`, req.url);
  tenantUrl.search = url.search;
  
  const response = NextResponse.rewrite(tenantUrl);
  
  // Injeta info do tenant como headers para consumo downstream
  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 {
    // Em produção, cache isto agressivamente
    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;
  }
}

O Padrão Matcher É Tudo

Você quer velocidade? Aquele regex config.matcher é seu melhor amigo. Sem ele, cada requisição arrasta o middleware junto — assets estáticos, imagens, tudo. E isso é uma viagem de mão única para 200ms+ de latência extra. Ninguém quer isso. Veja isto: /((?!_next/static|_next/image|favicon\\.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js)$).*). Apenas isto reduziu nossa latência P95 em 40%! Sim, é algo bem importante.

Por Que Rewrite Ao Invés de Redirect?

Reescrever não mexe com o que os usuários veem em seu navegador — estamos falando de navegação tranquila. Redirecionamentos? Aqueles são como pedir aos seus usuários que confiem que você os levará ao lugar certo — outra URL na caixa. Fique com rewrite() para uma experiência sem drama do usuário.

Subdomínio Wildcard Next.js Middleware para Multi-Tenant SaaS - arquitetura

Estratégias de Resolução de Tenant

O middleware acima arranca tenants de subdomínios como um profissional. Mas existência do tenant? Precisa confirmar isso. Diferentes abordagens para diferentes necessidades quando se trata de estratégias.

Estratégia 1: Busca em Banco de Dados no Middleware

Aqui está o detalhe: o middleware Next.js é executado no Edge Runtime, o que significa dizer adeus a muitas das APIs do Node.js.

// Funciona com Prisma Accelerate ou @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 },
  });
}

Estratégia 2: Busca em KV Store

Esta é minha favorita. Armazene seus slugs em um KV store como Vercel KV, Upstash Redis ou Cloudflare KV. Buscas na Edge em 1-5ms significam atraso negligenciável.

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

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

Estratégia 3: Lista de Permitidos Estática

Operação pequena? Listas de permitidos estáticas podem ser seu amigo — arquivos JSON em tempo de construção que mantêm as coisas apertadas e zero-rede.

import tenants from './tenants.json';

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

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

Reconstrua com ISR ou webhooks para lidar com novos tenants dinamicamente.

Design de Banco de Dados para Multi-Tenancy

É aqui que as coisas ficam sérias. Dois caminhos principais para escolher:

Banco de Dados Compartilhado com ID de Tenant

Adicione uma coluna tenantId como se estivesse em promoção:

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);

E sim, PostgreSQL RLS é sua apólice de seguro:

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

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

A vida fica mais fácil quando você define contexto:

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

Banco de Dados Por Tenant

Quer isolamento top-notch que se encaixa em requisitos de conformidade como uma luva? Um banco de dados por tenant é o caminho. O branching da Neon e o preço por banco de dados da PlanetScale tornam isso viável, mesmo para fortunas pequenas em 2026.

Suporte a Domínios Customizados

É aqui que deployment e vanity se encontram. Os tenants precisam que seus domínios customizados registrados em CNAME apontem seu caminho. E SSL? Cada link clicado precisa brilhar com esse HTTPS.

API de Domínios Customizados da Vercel

A API da Vercel torna isto praticamente indolor:

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) {
    // Armazene o mapeamento domínio-para-tenant
    await db.customDomain.create({
      data: { domain, tenantId, verified: false },
    });
  }
  return response.json();
}

Vercel cuida do SSL uma vez que o DNS se alinhe. Confira seu Platforms Starter Kit — é uma implementação modelo.

Cloudflare para SaaS

Isso é ideal para os gerenciadores de DNS customizado sofisticados, já que o "SSL for SaaS" do Cloudflare oferece provisionamento de SSL e proxies também.

Performance e Caching

Ao lidar com requisições, cada nanosegundo conta. O truque é acelerar a resolução de tenant.

Cache de Resolução de Tenant

Você salvará o dia (e o servidor) fazendo cache de buscas 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;
}

Lembre-se porém, caches em memória não são para Edge Runtime. Vá com algo como Upstash Redis — é o que eles fazem de melhor.

ISR e Multi-Tenancy

ISR adora multi-tenancy porque é inteligente. Cada tenant ganha uma versão em cache exclusivamente por estar reescrevendo de /dashboard para /acme/dashboard. Nenhuma configuração adicional, apenas aproveite a magia do Next.js.

Configurações de Deployment

Decisões, decisões. Vamos comparar opções de deployment:

Recurso Vercel Pro Cloudflare Pages Auto-Hospedado (Docker)
Domínios Wildcard ✅ (manual)
API de Domínios Customizados ❌ (manual)
Middleware na Edge ❌ (Node apenas)
SSL Automático ⚠️ Let's Encrypt
Preço $20/assento/mês + uso $5/mês + uso $50-200/mês servidor
Máximo de Domínios Customizados 50+ Ilimitado Ilimitado

Para um lançamento brilhante e rápido, Vercel Pro é seu ticket. Mas quando aqueles números sobem — mais usuários, mais requisições — Cloudflare Pages ou opções auto-hospedadas lhe dão flexibilidade e acessibilidade.

Navegamos maravilhas de multi-tenant com cada método, então se você está ponderando seu próximo passo, nossas capacidades de desenvolvimento Next.js podem ser seu guia.

Considerações de Segurança

Multi-tenancy é super legal até que não é. Cada tenant espera seus dados seguros — sem vazamentos, sem escorregões.

Checklist de Isolamento de Tenant

  1. Filtrar por ID de Tenant: Nunca confie apenas em URLs. Verificações de back-end importam também.
  2. PostgreSQL RLS: É como ter um detalhe de segurança no seu banco de dados 24/7.
  3. Desinfectar Subdomínios: Nunca escorregue — permita [a-z0-9-] apenas. Evite takeovers de subdomínio.
  4. Rate Limit Por Tenant: Headers dinâmicos para throttle de API podem salvar seu bacon.
  5. Log, Auditoria, Revisão: Cada operação de escrita deve dizer "peguei". A confiança instila confiança.
// Valida slug de tenant no 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 e Multi-Tenancy

Sua API serve muitos subdomínios, você precisa de CORS para não fazer birra:

// 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 do handler
}

Testando Middleware Multi-Tenant Localmente

Dores de cabeça abundam se o dev local não suporta subdomínios. Aqui está como você sonha novamente.

Opção 1: Editar `/etc/hosts`

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

Então atualize seu middleware para reconhecer .yourapp.local durante desenvolvimento:

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

Opção 2: Use nip.io ou sslip.io

Estes serviços puxam mapeamentos mágicos de IP:

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

Simples, não requer editar hosts.

Opção 3: Túnel Local com Subdomínios Customizados

Use ngrok (ou similar) para túneis rápidos:

ngrok http 3000 --hostname=*.your-dev-domain.ngrok.io

Testar não deve ser mais difícil que cenários do mundo real.

Escrevendo Testes de Integração

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

describe('middleware de tenant', () => {
  it('reescreve requisições de subdomínio para caminho de tenant', async () => {
    const req = new NextRequest('https://acme.yourapp.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('passa requisições de domínio raiz', async () => {
    const req = new NextRequest('https://yourapp.com/');
    const res = await middleware(req);
    
    expect(res.headers.get('x-middleware-rewrite')).toBeNull();
  });

  it('rejeita caracteres de subdomínio inválidos', async () => {
    const req = new NextRequest('https://acme--evil.yourapp.com/');
    const res = await middleware(req);
    
    expect(res.status).toBe(307); // Redirect
  });
});

Pensando na imagem maior? Independentemente de estar escolhendo entre Next.js ou Astro, considere explorar nossa jornada de insights de camada CMS headless.


Lembre-se, explorar significa questionar, adaptar e crescer. Nos procure para uma sessão de brainstorm ou aprofunde-se em nosso preço se você está mirando em uma parceria de primeira categoria.