# SaaS multi-tenant com Prisma e Next.js: o jeito chato e o jeito certo

> Três estratégias pra isolar dados de cliente num SaaS, qual eu escolhi pro HubMenu, e o middleware que evita SELECT esquecendo o tenant_id.

- URL: https://www.tesit.com.br/blog/multi-tenant-prisma-nextjs
- Autor: Diego Testi (TESIT LABS)
- Publicado em: 2026-04-26
- Tags: Prisma, Next.js, multi-tenant, Postgres, HubMenu

Quando você está construindo um SaaS B2B, a primeira pergunta de arquitetura é:
*"como eu separo os dados de cliente A do cliente B?"*. Errar isso significa
ou vazar dado entre tenants, ou ter um banco que não escala. Os dois são problema.

No **HubMenu** (SaaS de cardápios digitais) decidi cedo. Aqui vão as 3 opções
que considerei e por que escolhi a que escolhi.

## Opção 1: Banco por tenant

Cada cliente, um Postgres. Isolamento total — impossível vazar.

**Quando faz sentido:** SaaS enterprise com 10-50 clientes pagando muito,
exigência de compliance forte (LGPD/HIPAA), ou cliente que quer hospedar
em servidor próprio.

**Por que descartei:** HubMenu é self-serve, vai ter centenas de
restaurantes. 1 banco por restaurante = pesadelo de migração e custo.

## Opção 2: Schema por tenant

Um banco, vários schemas (`tenant_a.users`, `tenant_b.users`).
Isolamento médio — Postgres permite. Prisma suporta com `previewFeatures = ["multiSchema"]`.

**Por que descartei:** migrações ficam complicadas (precisa rodar em N schemas),
backup/restore vira artesanato. Ganho marginal em isolamento não compensa.

## Opção 3: Coluna `tenant_id` em tudo (escolhida)

Um schema, todas as tabelas com `tenantId`. Toda query filtra por ele.

```prisma
model Tenant {
  id        String   @id @default(cuid())
  slug      String   @unique
  menus     Menu[]
  users     User[]
}

model Menu {
  id        String   @id @default(cuid())
  name      String
  tenantId  String
  tenant    Tenant   @relation(fields: [tenantId], references: [id])
  // ...
  @@index([tenantId])
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  tenantId  String
  tenant    Tenant   @relation(fields: [tenantId], references: [id])
}
```

**Pró:** simples, escala, migrations triviais.
**Contra:** se você esquece de filtrar por `tenantId` numa query, vaza dado.

## Como evitar o vazamento: extension do Prisma

A solução é **não confiar em desenvolvedor lembrar**. Em vez disso, um
client-side guard:

```ts
// lib/db.ts
import { PrismaClient } from "@prisma/client";
import { headers } from "next/headers";

export function dbForTenant(tenantId: string) {
  return new PrismaClient().$extends({
    query: {
      $allModels: {
        async $allOperations({ args, query, model }) {
          // tabelas que NÃO têm tenantId (ex: Tenant em si)
          const skip = ["Tenant"];
          if (skip.includes(model ?? "")) return query(args);

          args.where = { ...args.where, tenantId };
          return query(args);
        },
      },
    },
  });
}
```

Toda query feita pelo client retornado **automaticamente** vira
`WHERE tenantId = ?`. Esquecer de filtrar deixa de ser possível.

## Resolvendo o tenant atual

Pega do subdomínio (`acme.hubmenu.com`) ou do path (`/acme/dashboard`).
Eu uso subdomain via middleware Next.js:

```ts
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(req: NextRequest) {
  const host = req.headers.get("host") ?? "";
  const subdomain = host.split(".")[0];

  // se for "www" ou domínio raiz, não tem tenant
  if (subdomain === "www" || subdomain === "hubmenu") {
    return NextResponse.next();
  }

  const res = NextResponse.next();
  res.headers.set("x-tenant-slug", subdomain);
  return res;
}
```

Server Component pega `headers()` e converte slug → tenantId via cache.

## O que aprendi do jeito difícil

- **Sempre `@@index([tenantId])`**. Sem isso, toda query vira full scan.
- **Cookie de auth tem que ter o tenantId.** Senão você deixa lacuna entre login e first-query.
- **Teste com 2 tenants em desenvolvimento.** Bug de vazamento só aparece quando você usa o produto como cliente diferente.

Multi-tenant não é difícil — é detalhista. Errar nos detalhes vaza dado em produção.
Acertar nos detalhes vira feature: isolamento de cliente, billing por uso, white-label, tudo cai no colo.
