Kodekanox

Kodekanox Studio: plataforma onde criadores organizam prompts, personagens e cenas para produzir anime com IA — com segurança e cobrança que sustentam um negócio real.

Rol: Desenvolvedor Full Stack2025–2026Equipe enxuta
Next.js 16 (Turbopack, App Router)TypeScript (strict)Supabase (Postgres + Auth + Storage)StripeResendUpstash RedisTailwind CSSVercel
60s
vida das URLs assinadas de arquivo
100%
acesso pago gated pelo webhook do Stripe
3 idiomas
PT/ES/EN sem prefixo de URL

01 · Contexto e problema

Kodekanox Studio é uma plataforma onde criadores organizam prompts, personagens e cenas para produzir conteúdo estilo anime com IA — a Studio automatiza o trabalho pesado para o criador focar só em criar.

É um SaaS pago de verdade: Next.js 16 com Turbopack e App Router (Server Components + Server Actions por padrão), Supabase como backend (Postgres, Auth, Storage), Stripe para cobrança, Resend para e-mail transacional, Upstash Redis para rate-limit e reCAPTCHA contra bots.

O desafio central: vender acesso por assinatura a uma ferramenta de IA mantendo os fornecedores de IA invisíveis na UI (marca neutra), com segurança em profundidade — e garantindo que só um pagamento verificado, nunca um redirect do navegador, libere acesso.

Restrições

Marca neutra: nenhuma menção a fornecedores de IA terceiros na UIproxy.ts precisa viver em src/ — na raiz, os gates de auth/MFA somemServer-first: lógica sensível só em Server Actions/Route HandlersMulti-moeda (BRL/USD/EUR) com moeda travada no servidorSubdomínios separados (kodekanox.com vs admin.kodekanox.com) + MFAi18n próprio sem prefixo de URL (cookie/geo), PT como forma canônica

02 · Decisões técnicas

1

Acesso liberado só pelo webhook do Stripe

Problema

O redirect do navegador depois do checkout é falsificável; não pode ser a fonte de verdade para liberar acesso a um produto pago.

Opções

Liberar acesso no redirect do checkout · polling do estado da sessão · webhook assinado do Stripe como única fonte

Decisão ✓

O webhook do Stripe (checkout.session.completed e afins) é a ÚNICA fonte que libera acesso, com upsert idempotente por owner_id; studio_subscriptions é read-only para o usuário — só o service-role escreve status/plano.

Trade-off

Existe um pequeno delay entre pagar e ver o acesso ativo (espera o webhook chegar), mas é a única forma de ninguém se autoconceder uma assinatura manipulando o cliente.

2

Segurança em profundidade para arquivos privados

Problema

A plataforma guarda e serve arquivos privados de criadores; um bypass de RLS ou um bucket mal configurado expõe conteúdo pago de outra pessoa.

Opções

Buckets públicos com URLs ofuscadas · RLS só no Postgres · RLS em tabelas e Storage + URLs assinadas + verificação de posse antes do service-role

Decisão ✓

RLS em tabelas e em Storage, buckets privados, URLs assinadas de 60s, upload em 3 passos (assinatura → PUT direto ao Storage → confirmação com verificação de magic bytes), e o service-role só age depois de verificar a posse do recurso.

Trade-off

Mais passos no fluxo de upload (três idas em vez de uma), em troca de nenhuma URL válida por mais de 60s e nenhum bypass de RLS como caminho padrão.

3

Um proxy único como muralha (idioma, subdomínio, MFA)

Problema

Precisava de um único ponto que resolvesse idioma, protegesse rotas (/creator, /admin), separasse subdomínios (app vs admin) e exigisse MFA do admin — sem duplicar essa lógica em cada rota.

Opções

Checagem por página · middleware clássico · src/proxy.ts como muralha única (Next.js 16)

Decisão ✓

src/proxy.ts concentra tudo: resolve idioma por cookie/geo, protege /creator e /admin, separa kodekanox.com de admin.kodekanox.com e exige MFA no admin. É intencional que viva em src/ — na raiz simplesmente não roda, e todos esses gates somem em silêncio.

Trade-off

Vira um ponto único de falha se quebrar, então ficou documentado no AGENTS.md como algo que nunca se mexe sem ler antes os docs locais do Next.js 16 (a versão trouxe breaking changes).

Arquitetura

UI · App Router (Server Components + Client onde precisa)
App · Server Actions + Route Handlers
Proxy · idioma, subdomínio, gates de auth/MFA
Infra · Supabase (Postgres + Auth + Storage) + Stripe + Resend + Upstash

03 · Upload de arquivo privado: assinatura, PUT direto e confirmação por magic bytes

Guardar e servir arquivos de usuário num produto pago não pode confiar na extensão ou no mimetype que o cliente declara — e não pode passar o peso do arquivo pelo servidor da aplicação sem necessidade.

O fluxo ficou em 3 passos: o cliente pede uma URL assinada (o servidor verifica posse e cota antes de assinar), o cliente sobe o arquivo direto pro Storage (o app-server nunca vê os bytes), e só depois o cliente confirma — momento em que o servidor verifica a assinatura real do arquivo (magic bytes) antes de marcá-lo válido.

Isso se soma a uma cota de storage única em GB por plano (não por tipo de arquivo) — decisão direta: um vídeo pesa como 4 imagens, então cobrar por tipo deixaria o custo correr na frente da receita do plano.

Resultado: nenhum bucket público, nenhuma URL assinada sobrevive além de 60s, e nenhum arquivo é confiável só pela extensão que carrega.

04 · Segurança e processo antes de builds sensíveis

Revisão de segurança formal + teste de intrusão A/B antes de mergear features sensíveis (pagamento, acesso) — um hábito interno antes de tocar essas áreas, não uma auditoria externa pontual.

Fluxo de git disciplinado: branch por feature, commitlint com scope-enum, pre-push bloqueando push direto a main, e merges em ordem quando há dependência entre PRs.

Marca neutra por decisão de produto: nenhuma menção a fornecedores de IA terceiros na UI — o stack de ferramentas é segredo do negócio, não só estética.

Regra dura de engenharia, nascida de um bug real: valores runtime nunca são exportados de módulos "use server" (só funções async) — um .map is not a function em produção foi o que fez essa regra virar permanente.

Demo interativa

Em breve: execute o código e veja-o rodando ao vivo, sem instalar nada.

main.dart
Run

Editor ao vivo (DartPad) — em breve

Em breve

04 · Resultados · antes / depois

acesso pago
redirect do navegador (falsificável)
só via webhook assinado do Stripe
1º login pós-checkout
corrida de cookies com redirect()
navegação dura (window.location), sem corrida
nav do studio após troca de idioma
cacheada em PT pelo layout server
Client Component, idioma correto
URLs de arquivo privado
assinadas, expiram em 60s

05 · Retrospectiva

Definiria desde o dia 1 que valores runtime nunca saem de módulos "use server": o bug real que virou essa regra custou um tempo de debug que dava pra evitar.

Trocaria o login por navegação dura mais cedo: a corrida de cookies com redirect() só apareceu testando o fluxo completo, do checkout até o primeiro acesso — não em isolamento.

Manteria o webhook do Stripe como única fonte de acesso desde a primeira versão: é tentador confiar no redirect do checkout porque 'funciona no happy path', mas é exatamente o caminho que alguém malicioso tentaria falsificar primeiro.

Caso anterior
Próximo caso