Project 01 · App + AI + Infrastructure
CertEngine
Studying for technical certifications is inefficient. Existing practice platforms have fixed question sets, don't adapt to your level, and rely on external services. CertEngine generates AI-powered practice exams — running entirely on my own GPU server.
// origin
Started as my own problem
Preparing for my n8n and Power Platform certifications, I tried several available practice platforms. They all had the same problem: a fixed question bank that never changes, no diagnosis of which domain you're weakest in, and no explanations that actually teach you why an answer is wrong.
The obvious solution was to build something that generated new questions based on my performance. And while building it, I got more interested in the underlying technical problem: how to connect an LLM to a real certification documentation corpus so the generated questions are relevant, not hallucinated.
CertEngine started to solve my own problem. It ended up being the most complete full-stack AI development exercise I've done — and it runs in production on the same homelab where I experiment with everything else.
// architecture
How it's built
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 doesn't have its own server — it uses the homelab's shared infrastructure. stack_postgres, stack_redis, and stack_ollama are the same containers used by Gitea, Outline, and GlitchTip. That reduces the RAM footprint and simplifies backups: pg_dump covers all schemas in a single command.
// stack
The complete stack
| Layer | Technology |
|---|---|
| Backend | FastAPI 0.115 + Python 3.12 (native async) |
| ORM | SQLAlchemy 2.0 async + Alembic 1.13 |
| Validation | Pydantic v2 |
| Auth | python-jose + passlib + Google OAuth (authlib) |
| Queue | Redis + RQ workers (generate_questions_task) |
| 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 (shared stack_postgres) |
| Vector search | pgvector 0.7 (optional embeddings) |
| LLM | Ollama · Qwen 2.5 7B Q4_K_M · RTX 3070 |
// decisions
8 decisions with documented trade-offs
| Decision | Chose | Why | Trade-off |
|---|---|---|---|
| FastAPI over Django | FastAPI | Native async for Ollama calls (3–10s latency). Auto-generated OpenAPI serves as spec during development. More explicit than Django for pure APIs. | Less "batteries included." More developer responsibility for configuring auth, ORM, and middleware. |
| React + Vite over Next.js | React + Vite | CertEngine doesn't need SEO — it's a private-use SPA. Next.js adds SSR, App Router, and RSC that are unnecessary complexity for this case. | If a public-facing landing with SEO is ever needed, migration or a separate static page would be required. |
| RQ over Celery | RQ | Jobs are few and predictable: generate questions, scrape certifications. Celery adds a beat scheduler and multiple queues that are unnecessary at this volume. | RQ is less scalable. If volume grows to 50+ concurrent jobs, migration to Celery would be needed. |
| SM-2 over FSRS | SM-2 | Well-documented, works for 100–500 cards. FSRS is more accurate but requires user training data to calibrate — not available in the MVP. | SM-2 is less accurate for long-term memory. Migrating to FSRS possible in v2 without schema changes. |
| BeautifulSoup over Playwright | BeautifulSoup | Microsoft Learn and Google Cloud pages render server-side. No headless browser needed. ~0.5s per page vs. ~3–5s for Playwright just to initialize. | If any provider migrates to a client-rendered SPA, a PlaywrightScraper fallback would be needed. |
| JWT in httpOnly cookies | httpOnly cookies | Mitigates XSS — frontend JavaScript can't read the token. More secure than localStorage where any injected script can access it. | Added complexity with CORS and SameSite policy. Requires configuring the nginx proxy to pass cookies. |
| pgvector as optional | pgvector (NULL = no embedding) | Enables semantic search when an embedding is present. If NULL, core functionality is unaffected — it's an enhancement, not a prerequisite. | Generating embeddings requires running an additional model (nomic-embed-text). Intentional technical debt. |
| Local Ollama over external API | Ollama + RTX 3070 | Full privacy — data never leaves the server. 83 tok/s vs. ~8 tok/s on CPU. No dependency on paid external APIs. | Local model (Qwen 2.5 7B) is less capable than GPT-4 or Claude. Requires specific hardware for acceptable inference. |
// learnings
What I didn't know before building this
The RAG bottleneck isn't the model
It's the quality of the context you pass it. 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 can cancel all async benefits. Learning to detect them was the steepest learning curve of the project.
Designing pgvector as optional from day 0 was right
Designing the feature as gracefully degradable prevented blocking core development while the embeddings pipeline wasn't ready. A pattern I'll replicate: design features as optional enhancements, not dependencies.
CertEngine runs on the same homelab infrastructure.