Multi-tenant

Isolamento por tenant com middleware, header/subdomain e estrategias de schema.

O PlazerCLI suporta arquitetura multi-tenant quando voce habilita Organizations. O isolamento de dados garante que cada organizacao (tenant) so acesse seus proprios dados. O PlazerCLI implementa a estrategia de shared database com discriminator column.

Estrategia de isolamento

O PlazerCLI usa a coluna organizationId em todas as tabelas que devem ser isoladas por tenant:

// prisma/schema.prisma
model Project {
  id             String       @id @default(cuid())
  name           String
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])
  createdAt      DateTime     @default(now())
  updatedAt      DateTime     @updatedAt

  @@index([organizationId])
}

model Document {
  id             String       @id @default(cuid())
  title          String
  content        String?
  organizationId String
  organization   Organization @relation(fields: [organizationId], references: [id])

  @@index([organizationId])
}

Middleware de tenant

O middleware extrai o tenant do header ou do JWT e injeta no request:

// apps/api/src/middleware/tenant.middleware.ts
export function tenantMiddleware(req: Request, res: Response, next: NextFunction) {
  // Estrategia 1: Header X-Tenant-ID
  const tenantId = req.headers['x-tenant-id'] as string;

  // Estrategia 2: Subdominio (tenant.app.com)
  // const tenantId = req.hostname.split('.')[0];

  // Estrategia 3: JWT claim (mais seguro)
  // const tenantId = req.user?.orgId;

  if (!tenantId) {
    return res.status(400).json({ error: 'Tenant ID is required' });
  }

  req.tenantId = tenantId;
  next();
}

// NestJS Guard equivalente:
@Injectable()
export class TenantGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const tenantId = request.user?.orgId || request.headers['x-tenant-id'];
    if (!tenantId) throw new ForbiddenException('Tenant not found');
    request.tenantId = tenantId;
    return true;
  }
}

Prisma com filtro automatico

O PlazerCLI gera um Prisma client estendido que aplica filtro de tenant automaticamente:

// apps/api/src/lib/prisma-tenant.ts
import { PrismaClient } from '@prisma/client';

export function createTenantPrisma(organizationId: string) {
  const prisma = new PrismaClient();

  return prisma.$extends({
    query: {
      $allModels: {
        async findMany({ args, query }) {
          args.where = { ...args.where, organizationId };
          return query(args);
        },
        async findFirst({ args, query }) {
          args.where = { ...args.where, organizationId };
          return query(args);
        },
        async create({ args, query }) {
          args.data = { ...args.data, organizationId };
          return query(args);
        },
        async update({ args, query }) {
          args.where = { ...args.where, organizationId };
          return query(args);
        },
        async delete({ args, query }) {
          args.where = { ...args.where, organizationId };
          return query(args);
        },
      },
    },
  });
}

// Uso em um controller:
app.get('/api/projects', tenantMiddleware, async (req, res) => {
  const db = createTenantPrisma(req.tenantId);
  const projects = await db.project.findMany(); // automaticamente filtrado
  res.json(projects);
});