AI-powered certification practice platform running on self-hosted infrastructure with local LLM inference.
Live at cert.ericmartinezf.dev
CertEngine generates personalized certification practice exams using RAG pipelines and local LLMs. Questions adapt to your performance over time using the SM-2 spaced repetition algorithm.
Built to solve a personal problem: existing practice platforms have fixed question banks, don't adapt to your level, and rely on external services. CertEngine runs entirely on my own GPU server — no external API calls, no data leaving the machine.
| Layer | Technology |
|---|---|
| Backend | FastAPI 0.115 + Python 3.12 (async) |
| ORM | SQLAlchemy 2.0 async + Alembic 1.13 |
| Validation | Pydantic v2 |
| Auth | python-jose + passlib + Google OAuth (authlib) |
| Queue | Redis + RQ workers |
| Frontend | React 18 + Vite 5 + TypeScript 5 |
| Styles | TailwindCSS + shadcn/ui |
| Data fetching | TanStack Query v5 |
| Router | React Router v6 |
| PWA | vite-plugin-pwa 0.20 |
| Database | PostgreSQL 16 + pgvector 0.7 |
| LLM | Ollama · Qwen 2.5 7B Q4_K_M · RTX 3070 |
| Spaced repetition | SM-2 algorithm |
| Reverse proxy | Traefik via Docker labels |
| Tunnel | Cloudflare Tunnel |
Browser / PWA
└── CloudFlared Tunnel
└── Traefik
└── certengine-web (Nginx + React SPA)
└── /api/* → certengine-api (FastAPI)
├── PostgreSQL 16 + pgvector
│ └── schema: certengine
├── Redis (cache + job queue)
│ └── RQ Worker
└── Ollama (Qwen 2.5 7B)
└── RTX 3070 · 83 tok/s
CertEngine shares infrastructure with the homelab stack:
stack_postgres, stack_redis, and stack_ollama are
the same containers used by Gitea, Outline, and GlitchTip.
This reduces RAM footprint and simplifies backups —
one pg_dump covers all schemas.
Async-native for Ollama calls (3–10s latency per request). Auto-generated OpenAPI schema serves as development spec. More explicit than Django for pure API work. Trade-off: Less "batteries included" — auth, ORM, and middleware require explicit configuration.
CertEngine is a private SPA — no SEO needed. Next.js adds SSR, App Router, and RSC complexity that's unnecessary for this use case. Trade-off: Adding a public landing page later would require a separate static page or migration.
AI jobs are few and predictable: generate questions, scrape certification docs. Celery adds beat scheduler and multiple queue overhead unnecessary at this volume. Trade-off: RQ doesn't scale beyond ~50 concurrent jobs well. Migration to Celery is possible without schema changes.
Well-documented, works well for 100–500 cards. FSRS is more accurate but requires user training data to calibrate — not available in MVP. Trade-off: SM-2 is less accurate for long-term memory retention. FSRS migration possible in v2 without schema changes.
Microsoft Learn and Google Cloud render certification content server-side — no headless browser needed. ~0.5s per page vs ~3–5s for Playwright initialization. Trade-off: If a provider migrates to a full JS SPA, a PlaywrightScraper fallback would be needed.
Mitigates XSS — JavaScript can't read the token. Safer than localStorage where any injected script can access credentials. Trade-off: More complexity with CORS and SameSite policy. Nginx proxy must be configured to pass cookies.
Semantic search works when embedding is present. If NULL, core functionality is unaffected. Allows deploying without the embeddings pipeline. Trade-off: Generating embeddings requires running an additional model (nomic-embed-text). Intentional technical debt.
Total privacy — certification data never leaves the server. 83 tok/s on RTX 3070 vs ~8 tok/s on CPU. No external API costs or rate limits. Trade-off: Local model (Qwen 2.5 7B) is less capable than GPT-4 or Claude. Requires specific hardware for acceptable inference speed.
The RAG bottleneck isn't the model — it's the context. Questions generated from clean scraped content are dramatically better than those from unprocessed text. The difference isn't in the prompt, it's in the input.
Async in FastAPI isn't magic, it's discipline. A single synchronous blocking function in the critical path cancels all async benefits. Learning to detect them was the steepest learning curve.
Designing pgvector as optional from day 0 was right. Gracefully degradable features prevent blocking core development while optional pipelines aren't ready. A pattern worth replicating.
This project is not designed for general deployment — it's built for my specific homelab infrastructure (stack_net Docker network, shared Postgres/Redis/Ollama).
The architecture decisions and code are shared for reference and learning purposes.
If you want to run something similar, the key dependencies are:
- PostgreSQL 16 with pgvector extension
- Redis for job queue
- Ollama with a capable model (7B+ recommended)
- Docker with a shared network for service communication
Eric Martínez Fonseca Digital Automation Tech Lead · Banco BCI Self-hosted infrastructure · AI automation