Guia prático para construir um SaaS multi-tenant com RBAC: Arquitetura, modelagem e exemplos

12 de fevereiro de 2025

Este artigo é um guia prático para quem deseja criar um SaaS multi-tenant moderno, com autenticação, controle de acesso baseado em papéis (RBAC), organizações, convites, projetos e permissões implementadas tanto no backend quanto no frontend. Aqui você encontra dicas de arquitetura, exemplos de código, sugestões de modelagem e melhores práticas para estruturar seu próprio SaaS, servindo como referência para projetos onde usuários podem criar e gerenciar projetos dentro de organizações.

Visão Geral da Arquitetura

  • Monorepo: Organização do código em múltiplos pacotes (backend, frontend, libs compartilhadas).

  • Backend: API REST com Fastify, Prisma, autenticação JWT, RBAC e documentação automática com Swagger.

  • Frontend: Next.js, consumo da API, checagem de permissões no client.

  • Banco de Dados: PostgreSQL, modelagem multi-tenant (usuários, organizações, membros, projetos).

  • RBAC: Controle de acesso por papéis (Owner, Admin, Member, Billing, Anonymous) usando CASL.

Estrutura do Monorepo

/apps
  /api         # Backend (Fastify, Prisma)
  /web         # Frontend (Next.js)
  ...
/packages
  /auth        # Lógica de autenticação e RBAC compartilhada
  /env         # Configuração de variáveis de ambiente
  ...
/prisma        # Schemas e migrations do banco

Vantagens do monorepo:

  • Compartilhamento de código (ex: regras de permissão).
  • Deploy e testes unificados.
  • Facilidade de manutenção.

Modelagem de Dados

Principais Entidades:

  • User: Usuário do sistema.

  • Organization: Organização (tenant).

  • Member: Relaciona usuário à organização com um papel (role).

  • Project: Projeto criado dentro de uma organização.

  • Invite: Convite para novos membros.

Exemplo de Schema (Prisma)

model User {
  id        String   @id @default(uuid())
  name      String?
  email     String   @unique
  password  String?
  members   Member[]
  // ...
}

model Organization {
  id      String   @id @default(uuid())
  name    String
  slug    String   @unique
  owner   User     @relation(fields: [ownerId], references: [id])
  ownerId String
  members Member[]
  projects Project[]
  // ...
}

model Member {
  id             String   @id @default(uuid())
  role           Role
  organization   Organization @relation(fields: [organizationId], references: [id])
  organizationId String
  user           User         @relation(fields: [userId], references: [id])
  userId         String
}

model Project {
  id             String   @id @default(uuid())
  name           String
  description    String
  organization   Organization @relation(fields: [organizationId], references: [id])
  organizationId String
  owner          User         @relation(fields: [ownerId], references: [id])
  ownerId        String
}

Backend: API, Autenticação e RBAC

Autenticação:

  • Suporte a e-mail/senha e OAuth (ex: GitHub).

  • JWT para autenticação de rotas protegidas.

  • Recuperação de senha via e-mail.

  • RBAC (Role-Based Access Control)

  • Papéis: Owner, Admin, Member, Billing, Anonymous.

  • Permissões centralizadas em um módulo compartilhado (/packages/auth).

Implementação de permissões com CASL:

export const permissions: Record<Role, PermissionsByRole> = {
  ADMIN: (user, { can, cannot }) => {
    can('manage', 'all')
    cannot(['transfer_ownership', 'update'], 'Organization')
    can(['transfer_ownership', 'update'], 'Organization', {
      ownerId: { $eq: user.id },
    })
  },
  MEMBER: (user, { can }) => {
    can(['create', 'get'], 'Project')
    can(['update', 'delete'], 'Project', { ownerId: { $eq: user.id } })
  },
  // ...
}

Rotas Principais

  • POST /users: Criação de usuário.

  • POST /organizations: Criação de organização.

  • POST /organizations/:slug/projects: Criação de projeto.

  • GET /organizations/:slug/projects: Listagem de projetos.

  • POST /invites: Envio de convite para membro.

Exemplo de Criação de Organização

app.post('/organizations', { ... }, async (request, reply) => {
  const userId = await request.getCurrentUserId()
  const { name, domain } = request.body
  // Validações...
  const organization = await prisma.organization.create({
    data: {
      name,
      slug: createSlug(name),
      ownerId: userId,
      members: { create: { userId, role: 'ADMIN' } }
    }
  })
  return reply.status(201).send({ organizationId: organization.id })
})

Frontend: Permissões e Experiência do Usuário

  • Next.js: Rotas protegidas, SSR/SSG, integração com API.

  • Checagem de Permissões: Antes de renderizar botões ou páginas, verifica se o usuário pode executar a ação.

Exemplo de Checagem de Permissão

const permissions = await ability()
if (permissions?.cannot('create', 'Project')) {
  redirect('/dashboard')
}

Exemplo de Exibição Condicional

{
  permissions?.can('create', 'Project') && (
    <Button>
      <Link href={`/org/${currentOrg}/create-project`}>Criar projeto</Link>
    </Button>
  )
}

Fluxos Principais do SaaS

Cadastro e Login:

  • Usuário cria conta ou faz login via OAuth.

Criação de Organização:

  • Usuário cria uma organização e se torna admin.

Convite de Membros:

  • Admin convida membros por e-mail, atribuindo papéis.

Criação de Projetos:

  • Membros com permissão criam projetos dentro da organização.

Gestão de Permissões

  • Ações disponíveis variam conforme o papel do usuário.

Billing (Opcional)

  • Gestão de cobrança por organização, projeto ou membro.

Exemplos de Código

Definição de Permissões (CASL)

import { AbilityBuilder, createMongoAbility } from '@casl/ability'

export function defineAbilityFor(user) {
  const { can, cannot, build } = new AbilityBuilder(createMongoAbility)
  if (user.role === 'ADMIN') {
    can('manage', 'all')
  }
  if (user.role === 'MEMBER') {
    can('create', 'Project')
    can(['update', 'delete'], 'Project', { ownerId: user.id })
  }
  // ...
  return build()
}

Middleware de Autenticação

app.addHook('preHandler', async (request) => {
  request.getCurrentUserId = async () => {
    const { sub } = await request.jwtVerify()
    return sub
  }
})

Criação de Projeto (Backend)

app.post('/organizations/:slug/projects', { ... }, async (request, reply) => {
  const { slug } = request.params
  const userId = await request.getCurrentUserId()
  // Busca organização, valida permissão...
  const project = await prisma.project.create({
    data: {
      name: request.body.name,
      description: request.body.description,
      organizationId: organization.id,
      ownerId: userId
    }
  })
  return reply.status(201).send({ projectId: project.id })
})

Boas Práticas e Dicas Finais

Centralize as regras de permissão em um único módulo compartilhado. Use migrations e schemas versionados para o banco de dados.

Documente a API com Swagger/OpenAPI.

Implemente testes automatizados para fluxos críticos (auth, RBAC, criação de entidades).

Utilize variáveis de ambiente para secrets e configurações sensíveis.

Garanta que o frontend respeite as permissões do backend, evitando exposição de ações não permitidas.

Pense em extensibilidade: novos papéis, entidades e integrações podem ser adicionados facilmente.

Conclusão

Construir um SaaS multi-tenant robusto exige atenção à arquitetura, segurança e experiência do usuário. Com uma base sólida de RBAC, monorepo e boas práticas, você estará pronto para escalar seu produto e atender múltiplos clientes com diferentes necessidades e níveis de acesso.