# 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.
**Autores:** Mauro Mequelussi
**Publicado:** 2026-07-15T12:00:00.000Z
**Atualizado:** 2026-07-20T15:00:47.810778055Z
**Tags:** vibe-coding, multi-tenant, permissoes, surrealdb, saas, rbac
---
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:

| Conceito | Responsabilidade |
| --- | --- |
| `user` | identidade da pessoa |
| `organization` | empresa ou espaço de trabalho |
| `permission` | ação permitida, como `billing/read` |
| `role` | conjunto de permissões dentro da organização |
| `member_of` | ví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.

::callout{type="info" title="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

::code-demo{language="surql" filename="database/multi-tenant.surql"}
```surql
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](https://surrealdb.com/blog/organizations-and-teams-for-your-surrealdb-app) segue a mesma direção e inclui organizações, papéis, permissões, convites e extensões para equipes, unidades e subsidiárias.

::read-more{slug="saas-multi-tenant-surrealdb" label="Continuar: permissões, isolamento e testes multi-tenant" placement="mid_content"}
## 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.

::code-demo{language="surql" filename="database/permissions.seed.surql"}
```surql
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?

::code-demo{language="surql" filename="database/functions/can.surql"}
```surql
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.

::code-demo{language="surql" filename="database/project.surql"}
```surql
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.

::callout{type="warning" title="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](/blog/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.**
::