Skip to main content
Esta página assume o Modelo de autenticação sobre Supabase como pré-requisito — em especial a seção “Isolamento multi-tenant — Row Level Security”, que não repetimos aqui. O que segue detalha as duas decisões de isolamento que ficam acima de RLS: entre parceiros, e entre produtos.

Dois eixos de isolamento, não um

“Multi-tenant” nesta plataforma significa duas coisas diferentes, e é fácil confundi-las:
  1. Isolamento entre parceiros — a Algar não pode ver, nem por acidente, dado de um segundo parceiro que venha a integrar depois. Esse é um problema horizontal: a mesma API, os mesmos produtos, múltiplos consumidores institucionais.
  2. Isolamento entre produtos — o dado de uma criança no HistorinhAI não pode ter caminho de acesso, nem teórico, através de um bug ou de uma credencial comprometida no projeto que serve NovelAI/NovelAudio. Esse é um problema vertical: sensibilidade de dado diferente exige raio de explosão (blast radius) diferente, independente de quantos parceiros existam.
O mecanismo de isolamento entre leitores individuais dentro de um mesmo produto (um usuário não vê a linha de outro) já é resolvido por RLS e está fora do escopo desta página — ver Modelo de autenticação.

Isolamento entre produtos: por que a decisão foi diferente para cada caso

NovelAI e NovelAudio vivem no mesmo projeto Supabase (“Novel”) desde o schema inicial. Isso não é um atalho — é a modelagem correta para o caso: os dois produtos servem o mesmo catálogo (a mesma novela, os mesmos capítulos, os mesmos finais — um em texto, outro narrado), frequentemente para o mesmo leitor (a mesma pessoa pode assinar os dois), e o conteúdo sensível em jogo é preferência de leitura de um adulto — dado pessoal, mas de baixa severidade se um bug de isolamento vazasse entre as duas superfícies do mesmo projeto.A tabela profiles marca a origem com uma coluna app_source (novelai ou novelaudio); a tabela subscriptions tem uma chave composta (user_id, app), porque os dois produtos vendem planos diferentes para a mesma pessoa. RLS continua isolando usuário de usuário normalmente — o que não existe é isolamento de infraestrutura entre os dois produtos, porque não faz sentido operacional pagar esse custo por dois produtos que compartilham 100% do catálogo e a mesma base de leitores.Trade-off aceito: menos overhead operacional (uma migration, um Auth, um Storage, uma fatura) em troca de isolamento mais fraco entre os dois produtos — aceitável porque o dado em jogo tem o mesmo nível de sensibilidade nos dois lados.
A regra geral por trás das duas decisões: o custo de isolar sobe com o overhead operacional; o benefício de isolar sobe com a severidade do dado. Quando os dois produtos compartilham nível de sensibilidade (NovelAI/NovelAudio), o projeto compartilhado vence. Quando um produto lida com dado de criança, a defesa em profundidade justifica o custo adicional mesmo que RLS, isoladamente, já resolvesse o problema no papel.

partner_id: isolando parceiros, não produtos

O eixo de parceiro é ortogonal ao eixo de produto descrito acima, e vive em uma camada diferente: o projeto Supabase “Platform” (dedicado à API de parceiro, descrito em Modelo de autenticação) guarda partners e partner_credentials — estruturalmente preparado para múltiplos parceiros desde o schema inicial. O entitlement em si (a coluna partner_id junto do subscriber_id) vive do lado de cada produto, na tabela partner_subscribers do respectivo projeto — não em uma tabela do Platform. O isolamento entre parceiros não é feito por policy de RLS comparando partner_id: as tabelas do domínio de parceiro têm RLS habilitado com zero policies — nenhum acesso para anon/authenticated; só as Edge Functions do Partner Gateway, via service_role, leem e escrevem nelas. O filtro por parceiro acontece dentro dessas functions, sempre a partir do JWT de serviço verificado, nunca de um valor enviado na requisição:
Isso isola um parceiro de outro com o mesmo rigor com que RLS isola um leitor de outro dentro de um produto — o mecanismo é diferente (verificação de JWT na function, em vez de policy por linha), mas a propriedade garantida é a mesma: nenhum caminho de acesso cruzado.
Como NovelAI/NovelAudio e HistorinhAI vivem em projetos Supabase separados do Platform, partner_id não pode ser uma foreign key de banco atravessando projetos — Postgres não tem FK entre instâncias diferentes. O limite é garantido pelo Partner Gateway: cada Edge Function de domínio (entitlements-novelai, entitlements-historinhai) valida a assinatura do JWT de serviço antes de aplicar sua própria lógica, e é essa validação — não uma constraint de banco — que impede um parceiro de agir sobre o partner_id de outro. Isso está descrito com mais detalhe na seção “Roteamento cross-projeto” do Modelo de autenticação.

Um segundo parceiro, sem mudança de schema

Quando um parceiro além da Algar quiser integrar — outra operadora, um banco, um varejista — o modelo já está preparado para isso, porque o desenho não codificou “Algar” em nenhum lugar do schema:
  1. Nova linha em partners (projeto Platform) — nome, escopos permitidos, status.
  2. Novo partner_credentialsclient_id/hash do client_secret para esse parceiro trocar por um JWT de serviço em /oauth2/token.
  3. Cada entitlement, cada evento de billing usage, cada webhook desse parceiro nasce com o partner_id dele — as mesmas tabelas, as mesmas policies, sem alteração de estrutura.
O que muda por parceiro não é schema — é configuração: escopos permitidos, limites de rate-limit na borda, e os termos comerciais/SLA específicos daquele contrato (que vivem fora do banco, no contrato em si). O mesmo vale no sentido inverso: um parceiro pode ativar qualquer subconjunto dos três produtos (só NovelAI, ou os três) sem exigir nenhuma tabela nova — partner_id mais o produto referenciado no entitlement já expressam essa combinação. O desenho descrito nesta página (ver Modelo de autenticação) já nasce pronto para múltiplos parceiros, mesmo antes do segundo parceiro existir.