Kodekanox

Kodekanox Studio: plataforma donde creadores organizan prompts, personajes y escenas para producir anime con IA — con seguridad y cobro que sostienen un negocio real, no un demo.

Rol: Desarrollador Full Stack2025–2026Equipo reducido
Next.js 16 (Turbopack, App Router)TypeScript (strict)Supabase (Postgres + Auth + Storage)StripeResendUpstash RedisTailwind CSSVercel
60s
vida de las URLs firmadas de archivo
100%
acceso pago gateado por el webhook de Stripe
3 idiomas
PT/ES/EN sin prefijo de URL

01 · Contexto y problema

Kodekanox Studio es una plataforma donde creadores organizan prompts, personajes y escenas para producir contenido estilo anime con IA — la Studio automatiza el trabajo pesado para que el creador se enfoque solo en crear.

Es un SaaS pago de verdad: Next.js 16 con Turbopack y App Router (Server Components + Server Actions por defecto), Supabase como backend (Postgres, Auth, Storage), Stripe para cobros, Resend para email transaccional, Upstash Redis para rate-limit y reCAPTCHA contra bots.

El desafío central: vender acceso por suscripción a una herramienta de IA manteniendo a los proveedores de IA invisibles en la UI (marca neutra), con seguridad en profundidad — y garantizando que solo un pago verificado, nunca un redirect del navegador, otorgue acceso.

Restricciones

Marca neutra: ninguna mención a proveedores de IA de terceros en la UIproxy.ts debe vivir en src/ — en la raíz, los gates de auth/MFA desaparecenServer-first: lógica sensible solo en Server Actions/Route HandlersMulti-moneda (BRL/USD/EUR) con moneda forzada del lado del servidorSubdominios separados (kodekanox.com vs admin.kodekanox.com) + MFAi18n propio sin prefijo de URL (cookie/geo), PT como forma canónica

02 · Decisiones técnicas

1

Acceso otorgado solo por el webhook de Stripe

Problema

El redirect del navegador tras el checkout es falsificable; no puede ser la fuente de verdad para otorgar acceso a un producto pago.

Opciones

Otorgar acceso en el redirect del checkout · polling del estado de la sesión · webhook firmado de Stripe como única fuente

Decisión ✓

El webhook de Stripe (checkout.session.completed y afines) es la ÚNICA fuente que otorga acceso, con upsert idempotente por owner_id; studio_subscriptions es read-only para el usuario — solo el service-role escribe status/plan.

Trade-off

Hay un pequeño delay entre pagar y ver el acceso activo (espera al webhook), pero es la única forma de que nadie se autoconceda una suscripción manipulando el cliente.

2

Seguridad en profundidad para archivos privados

Problema

La plataforma guarda y sirve archivos privados de creadores; un bypass de RLS o un bucket mal configurado expone contenido pago de otra persona.

Opciones

Buckets públicos con URLs ofuscadas · RLS solo en Postgres · RLS en tablas y Storage + URLs firmadas + verificación de propiedad antes del service-role

Decisión ✓

RLS en tablas y en Storage, buckets privados, URLs firmadas de 60s, subida en 3 pasos (firma → PUT directo a Storage → confirmación con verificación de magic bytes), y el service-role solo actúa después de verificar la propiedad del recurso.

Trade-off

Más pasos en el flujo de subida (tres viajes en vez de uno), a cambio de que ninguna URL quede válida más de 60s y que un bypass de RLS nunca sea el camino por defecto.

3

Un proxy único como muralla (idioma, subdominio, MFA)

Problema

Necesitaba un único punto que resolviera idioma, gateara rutas (/creator, /admin), separara subdominios (app vs admin) y exigiera MFA del admin — sin duplicar esa lógica en cada ruta.

Opciones

Chequeo por página · middleware clásico · src/proxy.ts como muralla única (Next.js 16)

Decisión ✓

src/proxy.ts concentra todo: resuelve idioma por cookie/geo, gatea /creator y /admin, separa kodekanox.com de admin.kodekanox.com y exige MFA en el admin. Es intencional que viva en src/ — en la raíz simplemente no corre, y todos esos gates desaparecen en silencio.

Trade-off

Se vuelve un punto único de falla si se rompe, así que quedó documentado en AGENTS.md como algo que nunca se toca sin leer antes los docs locales de Next.js 16 (la versión trajo breaking changes).

Arquitectura

UI · App Router (Server Components + Client donde hace falta)
App · Server Actions + Route Handlers
Proxy · idioma, subdominio, gates de auth/MFA
Infra · Supabase (Postgres + Auth + Storage) + Stripe + Resend + Upstash

03 · Subida de archivo privado: firma, PUT directo y confirmación por magic bytes

Guardar y servir archivos de usuario en un producto pago no puede confiar en la extensión o el mimetype que declara el cliente — y no debería pasar el peso del archivo por el servidor de la app sin necesidad.

El flujo quedó en 3 pasos: el cliente pide una URL firmada (el servidor verifica propiedad y cuota antes de firmar), el cliente sube el archivo directo a Storage (el app-server nunca ve los bytes), y recién después el cliente confirma — momento en que el servidor verifica la firma real del archivo (magic bytes) antes de marcarlo válido.

Esto se suma a una cuota de storage única en GB por plan (no por tipo de archivo) — decisión directa: un video pesa como 4 imágenes, así que cobrar por tipo dejaría que el costo corra por delante del ingreso del plan.

Resultado: ningún bucket público, ninguna URL firmada sobrevive más de 60s, y ningún archivo es confiable solo por la extensión que lleva.

04 · Seguridad y proceso antes de builds sensibles

Revisión de seguridad formal + test de intrusión A/B antes de mergear features sensibles (pago, acceso) — un hábito interno antes de tocar esas áreas, no una auditoría externa puntual.

Flujo de git disciplinado: rama por feature, commitlint con scope-enum, pre-push bloqueando push directo a main, y merges en orden cuando hay dependencia entre PRs.

Marca neutra por decisión de producto: ninguna mención a proveedores de IA de terceros en la UI — el stack de herramientas es secreto del negocio, no solo estética.

Regla dura de ingeniería, nacida de un bug real: los valores runtime nunca se exportan desde módulos "use server" (solo funciones async) — un .map is not a function en producción fue lo que hizo esa regla permanente.

Demo interactiva

Próximamente: ejecuta el código y míralo correr en vivo, sin instalar nada.

main.dart
Run

Editor en vivo (DartPad) — próximamente

Próximamente

04 · Resultados · antes / después

acceso pago
redirect del navegador (falsificable)
solo vía webhook firmado de Stripe
1er login post-checkout
carrera de cookies con redirect()
navegación dura (window.location), sin carrera
nav del studio tras cambio de idioma
cacheada en PT por el layout server
Client Component, idioma correcto
URLs de archivo privado
firmadas, expiran en 60s

05 · Retrospectiva

Definiría desde el día 1 que los valores runtime nunca salen de módulos "use server": el bug real que originó esa regla costó un tiempo de debug que se podía evitar.

Cambiaría el login por navegación dura más temprano: la carrera de cookies con redirect() solo apareció probando el flujo completo, desde el checkout hasta el primer acceso — no en aislamiento.

Mantendría el webhook de Stripe como única fuente de acceso desde la primera versión: es tentador confiar en el redirect del checkout porque 'funciona en el happy path', pero es justo el camino que alguien malicioso intentaría falsificar primero.

Caso anterior
Siguiente caso