Documéntalo
Guías

Multi-tenancy con Prisma y Next.js

Cómo implementar una arquitectura multi-tenant usando Prisma ORM

Multi-tenancy con Prisma y Next.js

Guía completa para implementar una arquitectura multi-tenant donde cada organización tiene sus datos aislados.

¿Qué es multi-tenancy?

Multi-tenancy permite que múltiples organizaciones (tenants) usen la misma aplicación con datos completamente aislados entre sí.

Estrategias comunes

EstrategiaDescripciónProsContras
Row-levelUna DB, filtrado por organizationIdSimple, económicoRiesgo de data leak
Schema-levelUna DB, un schema por tenantBuen aislamientoComplejidad media
Database-levelUna DB por tenantMáximo aislamientoCostoso, difícil de mantener

Esta guía cubre row-level tenancy, la más común para SaaS.

Modelo de datos

// schema.prisma

model Organization {
  id        String   @id @default(cuid())
  name      String
  slug      String   @unique
  createdAt DateTime @default(now())

  // Relaciones - todo pertenece a una org
  clients   Client[]
  providers Provider[]
  sessions  Session[]
}

model Client {
  id             String       @id @default(cuid())
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])

  name      String
  email     String

  // Index compuesto para queries eficientes
  @@index([organizationId])
  // Email único DENTRO de la organización
  @@unique([organizationId, email])
}

El patrón: Context de Organización

Crea un helper que siempre incluya el organizationId:

// lib/db/organization-context.ts

import { prisma } from '@/lib/prisma'

export function getOrgPrisma(organizationId: string) {
  return {
    client: {
      findMany: (args?: Parameters<typeof prisma.client.findMany>[0]) =>
        prisma.client.findMany({
          ...args,
          where: {
            ...args?.where,
            organizationId, // SIEMPRE se filtra por org
          },
        }),

      create: (args: Parameters<typeof prisma.client.create>[0]) =>
        prisma.client.create({
          ...args,
          data: {
            ...args.data,
            organizationId, // SIEMPRE se asocia a la org
          },
        }),

      // ... otros métodos
    },
  }
}

Uso en API Routes

// app/api/organizations/[orgSlug]/clients/route.ts

import { getOrganizationBySlug } from '@/lib/db/organizations'
import { getOrgPrisma } from '@/lib/db/organization-context'

export async function GET(
  request: NextRequest,
  { params }: { params: { orgSlug: string } }
) {
  const { orgSlug } = await params

  // 1. Validar auth
  const session = await getServerSession()
  if (!session) {
    return NextResponse.json({ error: 'No autorizado' }, { status: 401 })
  }

  // 2. Obtener organización
  const org = await getOrganizationBySlug(orgSlug)
  if (!org) {
    return NextResponse.json({ error: 'Org no encontrada' }, { status: 404 })
  }

  // 3. Usar prisma con contexto de org
  const db = getOrgPrisma(org.id)
  const clients = await db.client.findMany()

  return NextResponse.json(clients)
}

Validación en middleware

Para mayor seguridad, valida en middleware que el usuario pertenece a la org:

// middleware.ts

export async function middleware(request: NextRequest) {
  const orgSlug = request.nextUrl.pathname.split('/')[1]

  if (orgSlug && !orgSlug.startsWith('api')) {
    const session = await getToken({ req: request })

    if (session) {
      const hasAccess = await userBelongsToOrg(session.userId, orgSlug)
      if (!hasAccess) {
        return NextResponse.redirect(new URL('/unauthorized', request.url))
      }
    }
  }

  return NextResponse.next()
}

Errores comunes a evitar

Nunca hagas queries sin filtrar por organización. Un simple prisma.client.findMany() sin where: { organizationId } expone datos de TODOS los tenants.

Cuidado con los includes. Si haces include: { sessions: true }, asegúrate que las sesiones también estén filtradas o uses el contexto correcto.

Testing

// tests/multi-tenancy.test.ts

describe('Data isolation', () => {
  it('org A cannot see org B clients', async () => {
    const dbOrgA = getOrgPrisma('org_a_id')
    const dbOrgB = getOrgPrisma('org_b_id')

    // Crear cliente en org B
    await dbOrgB.client.create({
      data: { name: 'Cliente B', email: 'b@test.com' }
    })

    // Org A no debe verlo
    const clientsA = await dbOrgA.client.findMany()
    expect(clientsA).toHaveLength(0)
  })
})

Recursos adicionales

On this page