Esta página descreve conceitos, não o schema físico. Os nomes de tabela citados (
profiles,
novels, subscriptions…) existem dentro de cada projeto Supabase de produto — o vocabulário
de API (Subscriber, Entitlement, Event) é a camada de parceiro que se apoia neles. Onde os
dois convergem, dizemos explicitamente.Três identidades que parecem uma
O erro mais comum ao integrar é tratar “o assinante” como uma linha única. São três conceitos diferentes, em três lugares diferentes, e a API de parceiro existe justamente para costurá-los sem que quem integra precise saber onde cada um mora:
A relação entre os três é 1:N descendo: um
Subscriber pode, via SSO, corresponder a um User
em cada produto que ele ativa (o sub do token da Algar fica gravado em auth.identities, o
subscriber_id em app_metadata — ver Modelo de autenticação);
e um User tem exatamente um perfil por produto — exceto no HistorinhAI, onde o User é o
responsável (pai/mãe) e não quem consome o conteúdo.
- NovelAI / NovelAudio
- HistorinhAI
Aqui
User e “Reader/Listener” colapsam na prática: a tabela profiles é 1:1 com
auth.users, e a única diferença entre um “Reader” e um “Listener” é a coluna app_source
(novelai ou novelaudio) — porque os dois produtos compartilham o mesmo projeto Supabase
(ver Modelo multi-tenant). “Reader” e “Listener” são
vocabulário de API para diferenciar a intenção do produto, não duas tabelas diferentes.Conteúdo: Novel → Chapter → Page
O catálogo tem cinco níveis, com a granularidade pensada para separar o metadado de capítulo (usado pelo player de áudio) do conteúdo de leitura em si:- Category — agrupamento editorial do catálogo (ex: “coreana”, “fantasia”) — leitura pública, sem autenticação.
- Novel — a obra em si (título, sinopse, tags) — metadado público; conteúdo exige login.
- Chapter — a unidade do drip diário: um capítulo novo é liberado por dia, por leitor, por
obra (
unlocked_chapter_count). Guarda metadado (título, duração — usado pelo menu do player de NovelAudio). - Page — a unidade de conteúdo dentro do capítulo. Cada capítulo de conteúdo tem 7 páginas, liberadas juntas quando o capítulo é desbloqueado — não uma página por dia. Essa granularidade existe para separar “o que o motor de drip libera” (capítulo) de “como o conteúdo é paginado na leitura” (página), sem mudar o ritmo do drip nem o preço.
- Choice — os finais (tipicamente 4, por obra) vivem em um capítulo final próprio (o 8º
em
novel_chapters, só metadado — a tela de escolha, sem páginas de conteúdo), liberado no mesmo dia em que o último capítulo de conteúdo (o 7º) libera. Só ficam visíveis quando esse capítulo final libera para aquele leitor.
reading_progress (NovelAI) guarda o capítulo alcançado;
listening_progress (NovelAudio) guarda capítulo + posição em segundos dentro do áudio
pré-renderizado daquele capítulo. São conceitos de progresso deliberadamente separados, porque a
semântica de “onde você está” é diferente entre ler e ouvir.
HistorinhAI não compartilha esse grafo — seu catálogo é themes → stories, mais simples, porque
não há ramificação de narrativa nem drip diário: a seleção da história certa por perfil de criança
é decidida pela geração personalizada (RFP, página 17-19), não por um calendário fixo de
capítulos.
Entitlement: a ponte entre Subscriber e Product
Entitlement é o conceito que a API de parceiro expõe (POST /entitlements,
GET /subscriber-status/{id} — ver Platform API) para responder uma
pergunta: este Subscriber tem acesso a este Product agora?
Internamente, o entitlement de parceiro é gravado em uma tabela própria, partner_subscribers
(chave (partner_id, subscriber_id, product)), dentro do projeto Supabase de cada produto:
quando o Partner Gateway (projeto Platform) recebe POST /entitlements, ele roteia para a Edge
Function de domínio do produto correspondente (entitlements-novelai, por exemplo), que faz o
upsert nessa tabela. Isso é deliberado: enquanto o SSO não existe, não há user_id real para
ligar o subscriber_id do parceiro a uma conta do app — então nada toca a tabela subscriptions
do app oficial (que tem chave (user_id, app) e coluna source distinguindo app, stripe,
revenuecat). Quando o SSO existir, um passo de vínculo preencherá o user_id em
partner_subscribers e/ou provisionará subscriptions com source = 'partner' — reaproveitando
a lógica de gating (drip, TTS, streaming) que já lê subscriptions hoje, sem que nenhuma tabela
de domínio do leitor precise saber sobre parceiros.
Um Subscriber pode ter zero, um, ou até três Entitlements simultâneos (um por produto) — é isso
que permite ao parceiro ativar “só NovelAI” ou “os três produtos” para o mesmo assinante sem
nenhuma mudança de schema (ver Modelo multi-tenant).
Event: a base dos webhooks
Event é a abstração por trás de subscriber.updated e subscription.canceled (ver
Platform API — Eventos). Não existe uma tabela
events genérica — o mecanismo real é um Database Webhook (trigger Postgres via pg_net)
disparando na mudança de linha de partner_subscribers e invocando a função intermediária
webhook-dispatch (projeto Platform), que assina o payload com HMAC-SHA256 e o entrega ao
webhook_url do parceiro em uma única tentativa — não há fila própria de reentrega. Do ponto
de vista de quem integra, Event é o contrato estável (nome do evento + payload); do ponto de
vista da implementação, é uma consequência direta de uma escrita que já aconteceria de qualquer
forma — não um sistema de mensageria adicional para operar.
Visão consolidada
PARTNER (e as credenciais/configuração de webhook) vive no projeto Platform. SUBSCRIBER e
ENTITLEMENT colapsam, fisicamente, na tabela partner_subscribers de cada projeto de produto;
EVENT não é uma tabela — é o disparo do webhook na mudança dessas linhas. USER, PROFILE,
CHILD_PROFILE, NOVEL, CHAPTER e PAGE vivem dentro dos projetos Supabase de cada produto —
ver Modelo multi-tenant para onde exatamente cada um vive.