ApisDom Studio: Plataforma SaaS con Next.js 16, FastAPI y Claude Sonnet
Plataforma SaaS de generación de contenido con IA. Claude Sonnet 4.5 + Haiku 4.5, streaming SSE en vivo, editor TipTap, 15 tipos de contenido, adaptación a 7 redes sociales y checkout Stripe.
Si este proyecto aporta valor o podría inspirar a otros, considera difundirlo en tu red profesional.
¿Que es APISDOM Studio?
APISDOM Studio es una plataforma SaaS de generacion de contenido con IA que convierte un brief simple (tema, tono, audiencia, longitud) en un articulo profesional listo para publicar: HTML/Markdown estructurado, metadata SEO, resumen, tabla de contenidos, Q&A, bloques de codigo y fuentes web citadas. Cada generacion cruza la red en tiempo real por Server-Sent Events para que el usuario vea el texto aparecer palabra a palabra en el editor.
El sistema se divide en dos servicios independientes con fronteras explicitas:
Servicio
Responsabilidad
Hosting
Frontend Next.js (studio.apisdom.com)
UI, autenticacion, panel admin, editor TipTap, checkout Stripe, gestion de usuarios/creditos/perfiles/marcas, proxy firmado al backend
Motor de generacion, llamadas a Claude, busqueda web, streaming SSE, perfiles de generacion, historial, adaptacion a redes sociales
Cloud Run europe-west1, escala a cero
El motor usa dos modelos de Claude segun el tier del usuario: claude-haiku-4-5 para tier free y adaptacion a redes, claude-sonnet-4-5 para tier premium. El modelo puede sobrescribirse en vivo desde el panel de administracion sin redesplegar (override con TTL de 60s en AdminConfig, y una clave emergency_model_override que fuerza el modelo para todos los usuarios).
Que puede generar
15 tipos de contenido con 4 longitudes, 10 tonos, 4 niveles de audiencia y 2 idiomas (ES / EN), configurables por llamada o fijados en un perfil reutilizable.
Longitudes reales (definidas en schemas.py y mapeadas a max_tokens en engine.py):
Length
Palabras objetivo
max_tokens
short
300-500
2.000
medium
800-1.200
4.000
long
1.800-2.500
8.000
extra_long
3.000-5.000
16.000
Cada generacion devuelve un JSON estructurado con los mismos campos siempre: content_body, meta_seo (title, description, slug, Open Graph, keywords), summary, table_of_contents, sections, qa_items, code_blocks, sources, input_tokens, output_tokens, web_searches y model. Los campos opcionales (Q&A, codigo, TOC, resumen, fuentes) se activan por parametro en el request y se validan antes de llegar a Claude.
Arquitectura hexagonal (Ports & Adapters) de verdad
El frontend esta organizado en capas con una regla dura verificada en CI: core/ no puede importar de adapters/, lib/, ni paquetes externos. La estructura real de src/ es:
Backend: Google Cloud Run region europe-west1, despliegue automatico por Cloud Build desde push a main (imagen en Artifact Registry europe-west1-docker.pkg.dev).
Pagos: Stripe live (pk_live_...).
Email transaccional: Zoho SMTP via nodemailer.
Pipeline de generacion de contenido
Una llamada a POST /v1/generate lanza un flujo secuencial de 8 pasos que parte del brief del usuario y termina con un JSON completo persistido en historial. El frontend actua como proxy firmado hacia el backend FastAPI para que la clave del backend no toque nunca el navegador.
Paso
Proceso
Tecnologia
1. Autenticacion y creditos
/api/internal/generate/route.ts verifica Firebase Auth, calcula coste con CalculateCreditCost (tipo + longitud), bloquea si no hay creditos y descuenta atomicamente
Firebase Admin SDK + Firestore
2. Validacion de request
validate_generate_request + validate_topic_length rechazan combinaciones invalidas (include_code sin code_language, max_qa_items fuera de rango, topic vacio...) antes de llamar al modelo
Pydantic v2 + validadores custom
3. Seleccion de modelo
_get_model_name_async lee DB overrides con prioridad: emergency_model_override → model_<tier> → env var por defecto. TTL de cache 60s en AdminConfigRepository
SQLAlchemy async + cache
4. Construccion del prompt
build_system_prompt inyecta skill segun content_type, reglas de longitud, tono, audiencia, idioma, website del usuario, instrucciones custom y flags de Q&A / codigo / TOC / fuentes
Plantillas en prompts/seo-content-generator/
5. Web search condicional
should_enable_search + determine_search_config deciden si activar la tool nativa web_search_20250305 de Anthropic con tool_choice=any y max_sources configurables
Anthropic web_search tool
6. Llamada a Claude con fallback
Primer intento al modelo del tier. Si devuelve RateLimitError o APIStatusError 429/503, reintento automatico con el modelo alternativo sin perder el request
Anthropic SDK
7. Procesado de respuesta
Extraccion de tool_use_blocks, parseo del JSON estructurado, extraccion de citations nativas (web_search_result_location) con URL, titulo y texto citado
Parsers propios
8. Persistencia + respuesta
Guardado en historial (Generation SQLAlchemy) con input_tokens, output_tokens, web_searches, model y status. Respuesta final como JSON o stream SSE segun stream=true/false
SQLAlchemy + sse-starlette
Streaming SSE en tiempo real
Cuando el request trae stream: true, el backend devuelve StreamingResponse con text/event-stream y el frontend consume con EventSource. Cada evento lleva tipo y payload:
Evento
Contenido
meta
Modelo elegido, max_tokens, si hay web search activa
delta
Fragmento de texto generado (palabra / frase) — se pinta en vivo en el editor TipTap
tool_use
Claude lanzo una busqueda web (web_search tool call)
citation
URL + titulo + texto citado — aparece en el panel de fuentes mientras se genera
done
JSON final completo con todos los campos estructurados
error
Error capturado por wrap_sse_stream — se convierte en stream_error sin cortar la conexion bruscamente
El handler esta en streaming.py y el usuario ve el contenido aparecer progresivamente sin esperar a que Claude termine.
Pipeline de adaptacion a redes sociales (POST /v1/adapt)
Una vez generado un contenido, el usuario puede adaptarlo a 7 plataformas sociales sin coste extra para adaptaciones de 1 a 3 plataformas (1 credito) o 4 a 7 plataformas (2 creditos), definido en credit-costs.ts.
Paso
Proceso
1. Lectura del contenido origen
Se pasan content_body y metadata del contenido generado previamente
2. Limites nativos por plataforma
GET /v1/platforms devuelve el limite real de caracteres de Facebook, Instagram, X, LinkedIn, TikTok, YouTube, Pinterest
3. Llamada a Claude Haiku
Se usa claude-haiku-4-5 (anthropic_model_adapt) para reducir coste — la adaptacion no necesita razonamiento largo
4. Formato nativo
Hashtags, menciones, emojis, CTAs y saltos de linea segun las convenciones de cada plataforma
5. Respuesta multi-plataforma
JSON con una entrada por plataforma solicitada, cada una con text, character_count, hashtags y avisos de limite
Las adaptaciones no consumen web_search y no generan nuevas citations — trabajan sobre el contenido ya existente.
Perfiles de generacion reutilizables
Los usuarios (y el panel admin) pueden guardar profiles con configuracion preferida de tono, longitud, audiencia, idioma, style_guide, brand_voice y preferencias SEO. Cada request puede referenciar un perfil por profile_id y el backend los mergea con los parametros del request antes de construir el prompt. Endpoints en routes_profiles.py y UI en app/[locale]/profiles/.
Cada generacion queda registrada en el historial del usuario con todos los parametros, tokens consumidos, fuentes citadas y el JSON completo del resultado. El historial se expone en app/[locale]/history/ con filtros, re-descarga y re-uso como plantilla.
Editor TipTap y panel de administracion
APISDOM Studio tiene dos piezas de UI centrales: el editor WYSIWYG con TipTap donde el usuario ve y retoca el contenido generado por Claude, y un panel de administracion completo donde se gestionan usuarios, creditos, modelos, blog, partners y metricas globales.
Editor WYSIWYG — TipTap 3.20
El editor usa TipTap 3.20 con un conjunto completo de extensiones oficiales instaladas en produccion: StarterKit, Image, Link, Placeholder, Table + TableCell / TableHeader / TableRow, TextAlign y CodeBlockLowlight con resaltado de sintaxis via lowlight 3.3. Implementacion en editor/:
EditorContentArea.tsx — Area de edicion principal con useEditor, recibe el stream SSE del backend y va insertando nodos en vivo mientras Claude genera
EditorToolbar.tsx — Barra de formato: bold / italic / strike / headings / listas / quote / codigo / link / imagen / tabla / alineacion / undo-redo
EditorSidebar.tsx — Panel lateral con metadata SEO, TOC navegable, lista de fuentes citadas, Q&A y bloques de codigo detectados
FreeEditorSidebar.tsx — Variante para el modo editor libre sin generacion previa
HtmlSourceModal.tsx — Modal para editar el HTML crudo (sanitizado con isomorphic-dompurify antes de aplicarse)
EditorActions.tsx — Exportar a PDF (jsPDF), Markdown (marked), HTML plano, copiar al portapapeles, guardar como borrador
El contenido generado llega del backend en formato html / markdown / plain_text segun output_format del request, se parsea al schema de TipTap y el usuario puede seguir editando libremente. Todo lo que sale del editor pasa por isomorphic-dompurify antes de persistirse para evitar XSS.
Panel de administracion
Ruta /[locale]/admin protegida por rol de admin verificado en Firebase custom claims. La UI esta organizada en tabs en admin/:
Tab
Componente
Que permite
Estadisticas globales
GlobalStatsTab.tsx
Usuarios totales, generaciones hoy/semana/mes, tokens gastados, creditos vendidos, modelo mas usado
CRUD del blog publico en /blog con el mismo editor TipTap
Partners
partners/PartnersTab.tsx + 9 componentes
White-label completo: crear partners, planes, usuarios por partner, analytics, settings
El modelo activo y los max_tokens por longitud se pueden cambiar en caliente desde BackendConfigTab sin tocar env vars ni redesplegar — el backend lee el override con TTL de 60 segundos en AdminConfigRepository.
Partners / white-label
El sistema soporta partners con endpoints publicos bajo /api/v1/[partner]/** (generate, adapt, user/status) para integraciones de terceros con su propia base de usuarios, planes y analytics. Gestionados en el panel admin via 9 componentes dedicados: PartnerDashboard, PartnerDetail, PartnerPlans, PartnerUsers, PartnerSettings, PartnerGuideModal, CreatePartnerModal, PlanEditorModal, PartnersTab.
Cada partner tiene sus propios planes (creditos + precio), sus propios usuarios aislados y endpoints dedicados con autenticacion por API key. La separacion esta en la entidad Partner en core/entities/Partner.ts y PartnerPort en core/ports/PartnerPort.ts.
API publica para desarrolladores
Aparte de la app web, el backend expone una API REST publica documentada con OpenAPI 3 (/docs Swagger, /redoc ReDoc con logo custom inyectado en el schema). Los endpoints publicos son:
El motor de generacion vive en un microservicio independiente escrito en Python 3.12 + FastAPI, desplegado en Google Cloud Runeurope-west1 con escalado a cero. Esta aislado del frontend por dos razones: 1) la ANTHROPIC_API_KEY solo existe en el entorno del backend y jamas se expone al navegador, 2) el microservicio puede redesplegarse sin tocar el frontend (y al reves) porque Cloud Build tiene un filtro included files: backend/** en el trigger.
Estructura real
22 archivos Python · 6.214 lineas organizados por responsabilidad:
El middleware en app/auth/middleware.py distingue dos identidades que llaman al microservicio:
Nivel
Header
Secreto
Uso
ADMIN
Authorization: Bearer <ADMIN_API_KEY>
studio-admin-key (Secret Manager)
Acceso total, sin rate limit, endpoints /admin/** ocultos del OpenAPI publico
STUDIO
X-Studio-Key: <STUDIO_SECRET>
studio-secret (Secret Manager)
Llamadas del frontend Next.js como proxy firmado. Rate limits aplican
El frontend nunca expone estos secretos: los route handlers /api/internal/** de Next.js los leen de Firebase App Hosting Secret Manager y los adjuntan en servidor antes de llamar a STUDIO_API_URL. El navegador solo ve la llamada al propio dominio.
Headers adicionales para propagar contexto sin duplicar sesiones:
X-User-Id — propaga el UID de Firebase Auth al historial
X-User-Tier — free / premium, determina modelo Claude usado
X-Request-Id — trace id para correlacionar logs frontend ↔ backend
Modelo de datos (SQLAlchemy 2.0 async)
4 tablas, todas declarativas con Mapped[...] y tipado estricto. Definidas en app/models/database.py:
Tabla
Proposito
Campos clave
admin_config
Key-value editable en caliente desde el panel admin sin redesplegar
Se mergean con los parametros del request antes de llamar a Claude
generations
Historial completo de cada generacion con input/output, tokens, web searches, modelo y status
Consultable desde /api/internal/history del frontend y desde el panel admin
adaptations
Registro de cada adaptacion multi-plataforma con contenido origen y variantes generadas
Se enlaza con la generacion original por generation_id
El engine async soporta tanto SQLite (sqlite+aiosqlite:///./apisdom_studio.db en desarrollo) como PostgreSQL (postgresql+asyncpg://... en produccion) via la variable DATABASE_URL. Las tablas se crean en el lifespan de FastAPI con init_db() y la conexion se cierra limpiamente con close_db() al recibir SIGTERM de Cloud Run.
Docker multi-stage optimizado para Cloud Run
El Dockerfile usa build stage con gcc para compilar dependencias nativas + runtime stage python:3.12-slim con solo el virtualenv copiado. Usuario no-root appuser, PYTHONDONTWRITEBYTECODE=1, PYTHONUNBUFFERED=1, y un HEALTHCHECK que hace urlopen contra /health cada 30s. Arranque con uvicorn --workers 1 --timeout-keep-alive 30 (1 worker porque Cloud Run ya pone 1 contenedor por instancia con concurrency 80).
ANTHROPIC_API_KEY, ADMIN_API_KEY, STUDIO_SECRET desde Secret Manager
Pipeline de despliegue automatico (trigger en push a main con filtro backend/**):
Build de la imagen Docker
Push a Artifact Registry europe-west1-docker.pkg.dev/$PROJECT_ID/studio/api
Deploy a Cloud Run con la nueva revision
Limpieza: borra todas las revisiones excepto las 2 mas recientes
Limpieza: borra imagenes del registry excepto las 3 mas recientes
La limpieza automatica mantiene el coste de almacenamiento a cero sin intervencion manual — importante para un proyecto con despliegues frecuentes.
OpenAPI publico con branding custom
FastAPI genera automaticamente /docs (Swagger UI) y /redoc (ReDoc). En main.py se parchea el schema OpenAPI para inyectar:
info.x-logo — logo horizontal de APISDOM en ReDoc con enlace a https://studio.apisdom.com
Descripcion markdown extensa con tablas de tipos de contenido, idiomas, plataformas sociales y codigos de error
Tags organizados (generate, adapt, profiles) con descripciones detalladas
Rutas /admin/** ocultas del schema publico con include_in_schema=False
/health (shallow) y /health/deep (verifica DB + formato de ANTHROPIC_API_KEY) fuera del schema publico
El favicon.ico se sirve desde backend/favicon/ como FileResponse estatico para que las pestanas del navegador muestren la marca correctamente en /docs y /redoc.
mypy --strict — todas las funciones publicas con anotaciones completas, Any solo donde el SDK de Anthropic no expone tipos concretos
ruff con reglas de seguridad (S), bugbear (B), complejidad McCabe (C90), naming (N), pyupgrade (UP) e imports ordenados (I)
pytest + pytest-asyncio para cobertura de los flujos de generacion, adaptacion y perfiles
Verificadores automaticos de calidad
Cada cambio en el codigo del frontend pasa por una cadena de verificadores antes de poder commitear. El comando npm run verify:all los encadena en secuencia y un precommit de husky los dispara automaticamente. Definido en package.json:
"verify":"tsx scripts/verify-code-quality.ts","verify:types":"tsx scripts/verify-type-consistency.ts","verify:colors":"tsx scripts/find-hardcoded-colors.ts","verify:duplicates":"npx jscpd src --reporters console --min-lines 5 --min-tokens 50","verify:all":"npm run verify && npm run verify:types && npm run verify:colors && npm run type-check && npm run verify:duplicates","precommit":"npm run verify:all"
Que cubre cada verificador:
Script
Regla principal
verify-code-quality.ts
Maximo 40 lineas por funcion en .ts y 70 lineas por funcion en .tsx. Prohibe TODO, FIXME, HACK, WIP, TEMP. Exenciones explicitas para paginas legales, Footer y MobileMenu
verify-type-consistency.ts
Tipos alineados entre core/entities y los schemas del backend
find-hardcoded-colors.ts
Cero colores hardcodeados fuera del tema centralizado en config/
tsc --noEmit
TypeScript strict sobre los 458 archivos del proyecto
jscpd
Cero duplicacion de codigo con umbral de 5 lineas / 50 tokens
Con mypy --strict obligatorio: todas las funciones publicas llevan anotaciones de tipo completas y Any solo aparece donde el SDK de Anthropic no expone tipos concretos.
Cifras reales del proyecto
Todas las metricas estan contadas sobre el codigo real del repositorio, no estimadas.
Definido en pricing.ts — precios en centimos de euro:
Pack
Creditos
Precio
€/credito
Ahorro
Studio Starter
10
9,00 €
0,90 €
—
Studio Pro
50
39,00 €
0,78 €
13 %
Studio Business
200
129,00 €
0,645 €
28 %
Studio Enterprise
1.000
499,00 €
0,499 €
45 %
3 creditos gratis al registrarse (FREE_CREDITS_ON_SIGNUP = 3)
Adaptaciones multi-plataforma: 1 credito para 1-3 redes, 2 creditos para 4-7 redes
Coste variable por tipo + longitud: un social_post cuesta 1 credito, un article_long cuesta 5, un tutorial cuesta 4 — tabla completa en credit-costs.ts
Checkout con Stripe live (pk_live_...), webhook en /api/webhook/route.ts que procesa checkout.session.completed y abona los creditos atomicamente
Autenticacion: Firebase Auth en el cliente, Firebase Admin SDK en los route handlers server-side. El frontend jamas expone la clave del backend — actua como proxy firmado hacia STUDIO_API_URL
Secrets: gestionados por Firebase App Hosting Secret Manager, declarados como secret: ... en apphosting.yaml (STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, FIREBASE_ADMIN_PRIVATE_KEY, ZOHO_EMAIL_PASSWORD, IP_SALT, BACKEND_ADMIN_KEY...)
Sanitizacion de HTML: isomorphic-dompurify en todo input/output del editor