fayzfayz sdk

RLS e multi-tenant

Todo app Fayz é multi-tenant: vários clientes (tenants) compartilham o mesmo banco. O isolamento entre eles não é garantido no código da aplicação, e não vem de separar schemas — é garantido no banco, com Row Level Security (RLS) do Postgres. O RLS é a fronteira de isolamento do modelo: uma linha de um tenant é fisicamente invisível para os outros, mesmo que a aplicação erre.

A regra única: tenant_id + RLS

A regra é a mesma em todas as camadas do modelo de dados — core, tabelas plg_ de plugin e as tabelas do seu app: toda tabela carrega uma coluna tenant_id e tem uma policy de RLS chaveada nela. A forma canônica escopa a linha aos tenants a que o usuário logado pertence:

CREATE POLICY tenant_isolation ON <tabela>
  USING (tenant_id IN ( /* tenants do usuário logado */ ));

O conjunto de tenants do usuário vem de uma função no schema public, resolvida a partir do vínculo do usuário com cada tenant. A policy vale para SELECT, INSERT, UPDATE e DELETE. Resultado: uma query nunca enxerga uma linha de um tenant ao qual o usuário não pertence — o Postgres filtra antes de a aplicação ver.

O conceito — policy escopada por tenant_id ao conjunto de tenants do usuário — é estável. O nome exato da função e o texto SQL literal dependem da wave de pacotes instalada e podem mudar de rótulo; trate o SQL acima como a forma, não como uma assinatura fixa.

Website e admin: mesmas tabelas, papéis diferentes

O isolamento por tenant é só o primeiro eixo. O segundo é quem, dentro do tenant, vê o quê — e ele também mora no RLS, sobre as mesmas tabelas. O lado do site e o lado do admin não têm tabelas separadas (public_*/admin_*); têm policies e papéis diferentes sobre a mesma tabela: o staff enxerga as linhas do tenant, o cliente final enxerga só as próprias. É por isso que o Fayz nunca duplica tabelas por superfície — uma tabela, várias visões, todas garantidas pelo banco.

Canônico vs. deferido

Nem todo plugin escreve a policy inline, e isso é intencional:

  • canônico — a policy escreve a forma canônica (o tenant_id escopado ao conjunto de tenants do usuário) direto na migration do plugin.
  • deferido — o plugin habilita RLS (ENABLE ROW LEVEL SECURITY) mas não cria a policy; a tabela (com coluna tenant_id, tipo BASE TABLE, sem prefixo _) é capturada por um bloco de auto-detecção do app, que emite exatamente a policy canônica no momento do apply.

Um plugin deferido, portanto, aterrissa como canônico num banco real — não é uma divergência, só um deferimento de onde o texto da policy mora. As duas formas produzem o mesmo isolamento. Qualquer outra forma (divergent, no-rls) é tratada como problema a padronizar antes de mergear.

As tabelas do seu app também isolam

As tabelas que o seu app ou incubator cria não são exceção. Uma tabela de clientes ou de agendamentos que você adicione precisa carregar tenant_id e ter RLS. É fácil esquecer, porque essas tabelas são suas; a regra vale igual para elas — e para as tabelas plg_ dos plugins.

Migrations são append-only depois de aplicadas

Um detalhe operacional que evita corrupção silenciosa: uma migration que já rodou contra qualquer banco real é imutável. Corrija com uma migration nova, nunca editando o arquivo — o arquivo divergiria do banco em silêncio.

A exceção é antes do primeiro apply: um app greenfield, ou o schema-fonte de um plugin que ainda não rodou, pode ser editado no lugar (é até preferível — um apply novo deve já produzir a forma final). A regra de bolso: já tocou um banco vivo? → migration nova. Nunca aplicou? → edita no lugar.

Você quase nunca escreve essas policies à mão. O fayz db apply emite a forma canônica por você, e o padrão de tabelas do modelo (core, plg_, suas tabelas) já garante que toda tabela nova entre isolada. O que esta página te dá é o porquê — para você reconhecer uma tabela mal isolada quando vir uma.


Próximo: como o usuário logado é resolvido em Auth — visão geral.