Skip to main content
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.
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.
NovelAudio espelha exatamente esse grafo — mesma obra, mesmos capítulos, mesmos finais — só a unidade de progresso é diferente: 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 é themesstories, 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.