Proyecto 01 · App + IA + Infraestructura
CertEngine
Practicar para certificaciones técnicas es ineficiente. Los simulacros existentes tienen preguntas fijas, no se adaptan a tu nivel, y dependen de servicios externos. CertEngine genera exámenes de práctica con IA — corriendo completamente en mi propio servidor con GPU.
// origen
Empezó como un problema propio
Preparando mis certificaciones de n8n y Power Platform, probé varios simulacros disponibles. Todos tenían el mismo problema: un banco fijo de preguntas que no cambia, sin diagnóstico de en qué dominio estás más débil, y sin explicaciones que realmente enseñen por qué una respuesta es incorrecta.
La solución obvia era construir algo que generara preguntas nuevas en base a mi desempeño. Y mientras lo hacía, me interesó más el problema técnico de fondo: cómo conectar un LLM a un corpus de documentación real de certificaciones para que las preguntas generadas sean relevantes, no alucinadas.
CertEngine empezó para resolver mi propio problema. Terminó siendo el ejercicio más completo de desarrollo full-stack con IA que he hecho — y corre en producción en el mismo homelab donde experimento con el resto de las herramientas.
// arquitectura
Cómo está construido
Browser / PWA
└── CloudFlared Tunnel
└── Traefik
└── certengine-web (Nginx + React SPA)
└── /api/* → certengine-api (FastAPI + Uvicorn :8000)
├── stack_postgres (PostgreSQL 16 + pgvector)
│ └── schema: certengine
├── stack_redis (cache + job queue)
│ └── certengine-worker (RQ Worker)
└── stack_ollama (Qwen 2.5 7B · RTX 3070)
└── RAG pipeline + explicaciones SSE
Nginx hace proxy de /api/* al backend.
Todo corre en stack_net — la misma red Docker del homelab. CertEngine no tiene su propio servidor — usa la infraestructura compartida del homelab. stack_postgres, stack_redis y stack_ollama son los mismos containers que usan Gitea, Outline y GlitchTip. Eso reduce la huella de RAM y simplifica el backup: pg_dump incluye todos los schemas en un solo comando.
// stack
El stack completo
| Capa | Tecnología |
|---|---|
| Backend | FastAPI 0.115 + Python 3.12 (async nativo) |
| ORM | SQLAlchemy 2.0 async + Alembic 1.13 |
| Validación | Pydantic v2 |
| Auth | python-jose + passlib + Google OAuth (authlib) |
| Queue | Redis + RQ workers (generate_questions_task) |
| Frontend | React 18 + Vite 5 + TypeScript 5 |
| Estilos | TailwindCSS + shadcn/ui |
| Data fetch | TanStack Query v5 |
| Router | React Router v6 |
| PWA | vite-plugin-pwa 0.20 |
| Base de datos | PostgreSQL 16 (stack_postgres compartido) |
| Vector search | pgvector 0.7 (embeddings opcionales) |
| LLM | Ollama · Qwen 2.5 7B Q4_K_M · RTX 3070 |
// decisiones
8 decisiones con trade-off documentado
| Decisión | Elegí | Por qué | Trade-off |
|---|---|---|---|
| FastAPI sobre Django | FastAPI | Async nativo para llamadas a Ollama (3–10s de latencia). OpenAPI auto-generado sirve como spec para el desarrollo. Más explícito que Django para APIs puras. | Menos "baterías incluidas". Más responsabilidad en el desarrollador para configurar auth, ORM y middleware. |
| React + Vite sobre Next.js | React + Vite | CertEngine no necesita SEO — es una SPA de uso privado. Next.js agrega SSR, App Router y RSC que son complejidad innecesaria para este caso. | Si en el futuro se quiere una landing pública con SEO, habría que migrar o agregar una página estática separada. |
| RQ sobre Celery | RQ | Los jobs son pocos y predecibles: generar preguntas, scraping de certificaciones. Celery agrega beat scheduler y múltiples queues innecesarios para este volumen. | RQ es menos escalable. Si el volumen crece a 50+ jobs concurrentes, habría que migrar a Celery. |
| SM-2 sobre FSRS | SM-2 | Bien documentado, funciona para 100–500 tarjetas. FSRS es más preciso pero requiere datos de entrenamiento del usuario para calibrarse — no disponibles en el MVP. | SM-2 es menos preciso en memoria a largo plazo. Migrar a FSRS posible en v2 sin cambiar el schema. |
| BeautifulSoup sobre Playwright | BeautifulSoup | Las páginas de Microsoft Learn y Google Cloud renderizan en server-side. Sin browser headless. ~0.5s por página vs. ~3–5s de Playwright solo para inicializar. | Si algún proveedor migra a SPA sin SSR, habría que agregar PlaywrightScraper como alternativa. |
| JWT en httpOnly cookies | httpOnly cookies | Mitiga XSS — JavaScript del frontend no puede leer el token. Más seguro que localStorage donde cualquier script inyectado puede acceder. | Mayor complejidad con CORS y SameSite policy. Requiere configurar el proxy nginx para pasar cookies. |
| pgvector como opcional | pgvector (NULL = sin embedding) | Permite búsqueda semántica cuando el embedding está presente. Si es NULL, la funcionalidad principal no se ve afectada — es una mejora, no un prerequisito. | Generar embeddings requiere correr un modelo adicional (nomic-embed-text). Deuda técnica intencional. |
| Ollama local sobre API externa | Ollama + RTX 3070 | Privacidad total — los datos no salen del servidor. 83 tok/s vs. ~8 tok/s en CPU. Sin dependencia de APIs externas de pago. | El modelo local (Qwen 2.5 7B) es menos capaz que GPT-4 o Claude. Requiere hardware específico para inferencia aceptable. |
// aprendizajes
Lo que no sabía antes de construir esto
El cuello de botella de RAG no es el modelo
Es la calidad del contexto que le pasas. Preguntas generadas con scraping limpio son radicalmente mejores que las generadas con texto sin procesar. La diferencia no está en el prompt — está en el input.
Async en FastAPI no es magia, es disciplina
Una sola función síncrona bloqueante en el path crítico puede cancelar todos los beneficios de async. Aprender a detectarlas fue la curva más empinada del proyecto.
pgvector como opcional desde el día 0 fue correcto
Haber diseñado la feature como degradable evitó bloquear el desarrollo del core mientras el pipeline de embeddings no estaba listo. Es un patrón que voy a replicar: diseñar features como mejoras opcionales, no como dependencias.
CertEngine corre en la misma infraestructura del homelab.