> ## Documentation Index
> Fetch the complete documentation index at: https://developers.googa.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Modelo multi-tenant

> Como o isolamento funciona em dois eixos independentes — entre parceiros que integram a mesma API, e entre produtos com sensibilidade de dado diferente.

Esta página assume o [Modelo de autenticação sobre Supabase](/architecture/supabase-auth-model)
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](/architecture/supabase-auth-model).

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

<Tabs>
  <Tab title="NovelAI + NovelAudio: projeto compartilhado">
    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.
  </Tab>

  <Tab title="HistorinhAI: projeto isolado">
    HistorinhAI roda em um projeto Supabase próprio, sem nenhuma tabela compartilhada com o
    projeto "Novel". Essa é uma decisão deliberada, e a razão não é técnica no sentido de "RLS não
    funcionaria" — RLS funcionaria perfeitamente bem isolando linha por linha mesmo dentro de um
    projeto compartilhado. A razão é de **defesa em profundidade** dado o tipo de dado: perfil de
    criança (nome, data de nascimento, preferências, relatório de desenvolvimento comportamental)
    sob o Estatuto da Criança e do Adolescente, além da LGPD (ver
    [Proteção de dados de menores](/security/child-data-eca)).

    Com projetos separados, uma política de RLS mal configurada, uma Edge Function com um bug, um
    `service_role` key vazado, ou uma ação administrativa equivocada no projeto "Novel" **não têm
    caminho algum** — nem teórico — até uma tabela com dado de criança, porque essa tabela não
    está no mesmo banco. Isso transforma uma classe inteira de incidentes de "prevenível por
    política, se ela estiver correta" para "estruturalmente impossível, independente de qualquer
    política". É também uma narrativa de conformidade muito mais simples para auditoria e DPO:
    "dado de criança vive no próprio banco, ponto" é mais fácil de defender e de auditar do que
    "dado de criança vive no mesmo banco, protegido por políticas de aplicação".

    **Trade-off aceito:** mais overhead operacional (dois Auth, dois Storage, duas faturas, duas
    superfícies de migration) em troca de um limite físico que não depende de nenhuma política
    estar certa.
  </Tab>
</Tabs>

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](/architecture/supabase-auth-model)) 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:

```sql theme={null}
-- padrão real das tabelas de domínio de parceiro (Platform e produtos)
alter table public.partner_subscribers enable row level security;
-- RLS habilitado, ZERO policies: só service_role (Edge Functions) acessa.
```

```ts theme={null}
// dentro da Edge Function: partner_id vem do token verificado, nunca do payload
const claims = await requirePartnerToken(req, "subscriber:read");
supabase.from("partner_subscribers").select("...").eq("partner_id", claims.partnerId);
```

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.

<Warning>
  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](/architecture/supabase-auth-model).
</Warning>

## 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_credentials`** — `client_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](/architecture/supabase-auth-model)) já nasce pronto para
múltiplos parceiros, mesmo antes do segundo parceiro existir.
