Voltar aos materiais
TUTORIALVIBE-CODINGMULTI-TENANTPERMISSOES

SaaS multi-tenant no SurrealDB: organizações, times e permissões

Modele organizações, membros, papéis, convites e isolamento de dados no SurrealDB para que o seu SaaS cresça sem misturar clientes ou permissões.

Por Mauro Mequelussi

9_MIN
SaaS multi-tenant no SurrealDB: organizações, times e permissões
Em resumo

Modele organizações, membros, papéis, convites e isolamento de dados no SurrealDB para que o seu SaaS cresça sem misturar clientes ou permissões.

Todo SaaS B2B começa parecendo um sistema de um usuário só. Então chega o primeiro cliente com três funcionários. Depois, uma agência pede acesso a cinco empresas. Logo surgem convites, equipes, papéis, filiais e a pergunta que define o risco do produto: esta pessoa pode fazer esta operação dentro desta organização?

No SurrealDB, um SaaS multi-tenant pode representar usuários, organizações e papéis como registros; vínculos de trabalho como relações de grafo; e permissões como dados consultáveis. Isso aproxima o controle de acesso do modelo do negócio — desde que isolamento e testes sejam tratados como requisitos, não como detalhes do painel.

O que é multi-tenancy?

Multi-tenancy é a arquitetura em que uma aplicação atende várias organizações, chamadas tenants, mantendo dados e permissões separados. Uma mesma pessoa pode pertencer a mais de uma organização e ter um papel diferente em cada uma.

Exemplos brasileiros aparecem em quase todo setor:

  • um escritório contábil atende várias empresas;
  • uma agência gerencia clientes diferentes;
  • uma rede de clínicas possui unidades e profissionais;
  • uma escola divide turmas, coordenação e responsáveis;
  • um software financeiro separa contas por CNPJ.

O erro clássico é colocar organization_id nas tabelas e considerar o problema resolvido. Esse campo ajuda no escopo, mas não define sozinho quem pode convidar pessoas, exportar relatórios, gerenciar cobrança ou enxergar uma filial.

Um modelo que separa identidade, vínculo e autorização

Uma base sustentável usa cinco conceitos:

ConceitoResponsabilidade
useridentidade da pessoa
organizationempresa ou espaço de trabalho
permissionação permitida, como billing/read
roleconjunto de permissões dentro da organização
member_ofvínculo entre pessoa e organização, com papel e status

Esse desenho evita colocar um campo global is_admin no usuário. Alguém pode ser proprietário da própria empresa, analista numa empresa cliente e não ter acesso a uma terceira.

Permissão pertence ao contexto
“Mauro é admin” quase nunca é uma regra completa. A regra útil é “Mauro pode gerenciar membros da organização X porque o vínculo dele aponta para um papel que contém essa permissão”.

Schema base no SurrealDB

database/multi-tenant.surqlsurql
DEFINE TABLE user SCHEMAFULL;
DEFINE FIELD name ON user TYPE string;
DEFINE FIELD email ON user TYPE string
  ASSERT string::is_email($value);
DEFINE INDEX user_email ON user FIELDS email UNIQUE;

DEFINE TABLE organization SCHEMAFULL;
DEFINE FIELD name ON organization TYPE string;
DEFINE FIELD slug ON organization TYPE string;
DEFINE FIELD owner ON organization TYPE record<user>;
DEFINE FIELD created_at ON organization TYPE datetime
  DEFAULT time::now();
DEFINE INDEX organization_slug ON organization FIELDS slug UNIQUE;

DEFINE TABLE permission SCHEMAFULL;
DEFINE FIELD name ON permission TYPE string;
DEFINE FIELD description ON permission TYPE string;
DEFINE INDEX permission_name ON permission FIELDS name UNIQUE;

DEFINE TABLE role SCHEMAFULL;
DEFINE FIELD organization ON role TYPE record<organization>;
DEFINE FIELD name ON role TYPE string;
DEFINE FIELD permissions ON role TYPE array<record<permission>> DEFAULT [];
DEFINE INDEX role_name_per_org ON role
  FIELDS organization, name UNIQUE;

DEFINE TABLE member_of SCHEMAFULL
  TYPE RELATION IN user OUT organization;
DEFINE FIELD role ON member_of TYPE record<role>;
DEFINE FIELD status ON member_of TYPE string
  ASSERT $value IN ['invited', 'active', 'suspended'];
DEFINE FIELD joined_at ON member_of TYPE option<datetime>;
DEFINE INDEX membership_unique ON member_of FIELDS in, out UNIQUE;

A relação member_of é mais que uma tabela de junção: ela representa o contrato atual entre uma pessoa e uma organização. Status, cargo, data de entrada e papel pertencem ao vínculo, não ao usuário.

O template oficial de Organizations e Teams do SurrealKit segue a mesma direção e inclui organizações, papéis, permissões, convites e extensões para equipes, unidades e subsidiárias.

[ CONTEÚDO_INTEGRAL_DISPONÍVEL ]

Continuar: permissões, isolamento e testes multi-tenant

CLIQUE PARA DESBLOQUEAR A LEITURA

Construindo aplicações robustas com arquitetura de alto nível e infraestrutura otimizada na edge do Cloudflare Workers com SurrealDB.

Segurança de dados e conformidade com privacidade desde o primeiro dia de código com tratamento de estados e sessões resilientes.

Métricas de performance para monitorar a experiência do usuário e otimizar custos operacionais em produção com dashboards estruturados.

Continuação liberada

Permissões como dados deixam o SaaS evoluir

Quando permissões ficam presas num enum do código, cada novo recurso exige alteração e deploy coordenados. Como registros, elas podem ser criadas em seed e agrupadas em papéis por organização.

database/permissions.seed.surqlsurql
UPSERT permission:members_read CONTENT {
  name: 'members/read',
  description: 'Visualizar membros da organização'
};

UPSERT permission:members_manage CONTENT {
  name: 'members/manage',
  description: 'Convidar, alterar e suspender membros'
};

UPSERT permission:billing_read CONTENT {
  name: 'billing/read',
  description: 'Visualizar plano, faturas e cobrança'
};

UPSERT permission:billing_manage CONTENT {
  name: 'billing/manage',
  description: 'Alterar plano e dados de cobrança'
};

Um papel “Financeiro” pode ter leitura e gestão de cobrança sem receber acesso a membros. Um papel “Gestor da unidade” pode enxergar somente dados de uma filial. O nome amigável do papel pode variar; as permissões continuam sendo contratos estáveis.

Como verificar uma autorização

Autorização deve responder a três perguntas:

  1. qual é o usuário autenticado?
  2. qual é a organização da operação?
  3. o vínculo ativo aponta para um papel com a permissão necessária?
database/functions/can.surqlsurql
DEFINE FUNCTION fn::can(
  $user: record<user>,
  $organization: record<organization>,
  $permission_name: string
) {
  RETURN array::len(
    SELECT VALUE id
    FROM member_of
    WHERE in = $user
      AND out = $organization
      AND status = 'active'
      AND $permission_name IN role.permissions.name
    LIMIT 1
  ) > 0;
};

Adapte e valide a sintaxe à versão usada no projeto. Mais importante que copiar a função é testar o comportamento: acesso permitido no tenant correto, negado no tenant errado e negado quando o vínculo está suspenso.

O isolamento precisa chegar a cada registro

Considere uma tabela de projetos. Todo projeto deve pertencer a uma organização, e toda consulta pública precisa ser escopada por ela.

database/project.surqlsurql
DEFINE TABLE project SCHEMAFULL;
DEFINE FIELD organization ON project TYPE record<organization>;
DEFINE FIELD name ON project TYPE string;
DEFINE FIELD status ON project TYPE string
  ASSERT $value IN ['draft', 'active', 'archived'];
DEFINE FIELD created_by ON project TYPE record<user>;
DEFINE FIELD created_at ON project TYPE datetime DEFAULT time::now();

DEFINE INDEX project_by_org ON project FIELDS organization;

Não aceite organization do formulário como verdade. O servidor deve derivar o tenant do contexto autorizado e verificar o vínculo. Caso contrário, trocar um ID na requisição pode mover ou criar dados dentro de outra empresa.

Filtro não substitui autorização
WHERE organization = $org só é seguro se $org veio de um contexto que o usuário pode acessar. Um parâmetro enviado pelo navegador não prova pertencimento.

Convites precisam ser tratados como credenciais

Um convite é uma autorização pendente. Ele deve ter token imprevisível, validade, uso único, organização, papel e destinatário. Evite guardar o token puro se um hash atender ao fluxo. Ao aceitar:

  1. valide expiração e uso anterior;
  2. confirme a identidade ou o e-mail esperado;
  3. crie o vínculo de forma idempotente;
  4. marque o convite como usado na mesma transação;
  5. registre o evento para auditoria.

Enviar o mesmo formulário duas vezes não pode criar duas memberships. O índice único em member_of(in, out) funciona como última barreira, mas o fluxo também precisa responder de forma previsível à repetição.

Equipes, unidades e subsidiárias: quando adicionar?

Não modele uma hierarquia corporativa inteira no primeiro dia.

  • Equipes fazem sentido quando pessoas colaboram em subconjuntos de projetos.
  • Unidades fazem sentido para filiais, departamentos ou regiões com dados próprios.
  • Subsidiárias entram quando uma empresa-mãe precisa delegar acesso a organizações juridicamente ou operacionalmente separadas.

Comece com organização, membership e papel. Adicione outro nível quando existir uma pergunta concreta de autorização que o nível atual não consegue responder.

LGPD, auditoria e suporte

Multi-tenancy aumenta o impacto de qualquer falha: um bug pode expor dados de uma empresa para outra. Além de permissões, planeje:

  • log de alterações sensíveis com ator, tenant e horário;
  • exportação e exclusão com escopo correto;
  • retenção de convites e contas suspensas;
  • suporte administrativo sem “virar” qualquer usuário silenciosamente;
  • mascaramento de dados em ambientes não produtivos;
  • alertas para volume incomum de leitura ou exportação.

Logs também contêm dados. Não registre token de convite, sessão, documento completo ou corpo indiscriminado de requisições.

Testes que um SaaS multi-tenant precisa ter

Uma suíte mínima deve provar:

  1. membro ativo lê dados da própria organização;
  2. o mesmo membro não lê dados de outra organização;
  3. membro sem billing/read não vê cobrança;
  4. membro suspenso perde acesso;
  5. convite expirado falha;
  6. convite repetido não duplica vínculo;
  7. usuário em duas organizações recebe permissões diferentes;
  8. trocar o ID da organização na requisição não contorna autorização;
  9. exclusão de usuário não deixa relações órfãs sem decisão explícita;
  10. tarefas internas não usam um tenant recebido sem validação.

Peça para a IA gerar esses testes, mas confira se eles exercitam a conexão com privilégios reais. Testar tudo como root apenas prova que root pode fazer tudo.

Perguntas frequentes sobre multi-tenancy no SurrealDB

Devo criar um banco por cliente?

Depende do contrato, escala e isolamento exigido. Um banco compartilhado com tenant explícito simplifica operação e visão agregada; bancos separados aumentam isolamento, mas multiplicam migrações, conexões e observabilidade. Não escolha sem mapear backup, suporte e custo.

Papel e permissão são a mesma coisa?

Não. Permissão representa uma ação; papel agrupa permissões. Isso permite que cada organização tenha papéis como “Financeiro” ou “Operações” sem mudar o código da aplicação.

Posso confiar apenas na API para separar tenants?

Uma API bem construída é uma camada importante, mas permissões e invariantes próximas dos dados criam defesa em profundidade. A falha de uma rota não deve transformar o banco inteiro em leitura pública.

Preciso de equipes desde o MVP?

Geralmente não. Implemente primeiro organização, vínculo e papel. Equipe só entra quando usuários realmente precisam compartilhar uma parte do tenant com regras diferentes.

Uma base pequena, mas impossível de atravessar

O melhor MVP multi-tenant não é o que tem a matriz de permissões mais sofisticada. É o que consegue provar que uma pessoa nunca atravessa a fronteira da organização errada.

Comece pelo guia SurrealDB para vibe coding, modele um único recurso com tenant obrigatório e escreva os testes negativos antes de construir o painel de administração.

Do vibe ao produto. Com método.

Comece a construir

Quer transformar uma ideia em produto? O kit Sniper Apps reúne base, componentes e método para publicar com mais velocidade.

CONHECER_O_KIT →
Continue explorando
Ver todos os artigos →