How MIRROR is built, what it costs, and why — documented like a client deliverable.
MIRROR is a production RAG application, not a static portfolio. Generation runs on the xAI Grok API, retrieval is hybrid BM25 (SQLite FTS5) with optional API embeddings, state lives in SQLite (WAL), and the whole system is deployed with Docker Compose + Caddy (automatic TLS) on a small VPS. Nothing heavier than a Python process runs on the server, and a hard daily budget caps LLM spend below $0.50/day.
| Layer | Choice | Why |
|---|---|---|
| LLM | xAI Grok API (grok-4.20, non-reasoning) | Frontier quality, ~1s first token, zero RAM footprint, pay-per-use |
| Retrieval | SQLite FTS5 · BM25, hybrid-ready | In-process, 0 services to operate, right-sized for the corpus |
| Embeddings | Optional, via API (OpenAI-compatible) | No local model; activates hybrid dense+lexical scoring with one env var |
| App state | SQLite (WAL) - sessions, conversations, budget | One file, atomic, zero ops |
| Serving | Flask + gunicorn (2 workers × 8 threads) | I/O-bound workload: threads wait on the API, not the CPU |
| Edge | Caddy 2 | Automatic HTTPS/Let's Encrypt, 10-line config |
| Host | Small VPS (2-12 GB RAM), Docker Compose | Total infra + inference cost: under ~$1/day |
The most important engineering decision in this project was killing my own first architecture. V1 ran a fully sovereign stack; V2 runs entirely on APIs. Both are legitimate patterns - I have shipped both professionally (sovereign LLM on Scaleway for an enterprise client with data-residency constraints; API-first for products where speed of iteration wins). The point of this page is showing how to choose.
| V1 - Sovereign (self-hosted) | V2 - API-first (current) | |
|---|---|---|
| LLM | Phi-4 14B GGUF via llama.cpp, CPU | xAI Grok API |
| Embeddings | BGE-M3 (567M) via sentence-transformers | None required (BM25); API embeddings optional |
| Reranker | CrossEncoder MiniLM on CPU | Not needed at this corpus size |
| Vector store | Qdrant (Docker, HNSW + INT8) | SQLite FTS5, in-process |
| Server needed | 64 GB RAM / 12 cores | Any 2 GB VPS |
| RAM footprint | ~15 GB (models + Qdrant) | < 300 MB |
| First token | 5-20 s (CPU inference) | < 1.5 s |
| Answer quality | 14B quantized - honest but limited | Frontier model |
| Docker image | ~8 GB (torch + weights) | ~450 MB |
| Monthly cost | ~$60-100 (big server) | ~$5 VPS + ≤$15 API (capped) |
| Data sovereignty | Total | Prompts leave the server |
Generation uses the OpenAI-compatible xAI endpoint with grok-4.20-non-reasoning:
for short conversational answers, a reasoning model burns output tokens (and seconds) thinking
without improving a CV answer. The non-reasoning variant is the right latency/cost point.
stream_options.include_usage), not from estimates; pricing is configuration, not a hardcoded constant.| Item | Value |
|---|---|
| Input price | $1.25 / 1M tokens |
| Output price | $2.50 / 1M tokens |
| Typical RAG turn | ~3K in + ~400 out ≈ $0.005 |
| Daily cap ($0.50) | ≈ 100 RAG conversations/day |
The knowledge base (CV, 7 technical articles in 2 languages, architecture docs, visitor uploads) is a few hundred chunks. At this scale, a dedicated vector database is over-engineering - the honest engineering answer is BM25 on SQLite FTS5, in-process, with a design that upgrades to hybrid dense retrieval without a migration.
chunks table; an FTS5 virtual table (unicode61 tokenizer, diacritics-insensitive - matters for French) is kept in sync by triggers.bm25() scores are mapped to (0,1].EMBEDDINGS_API_KEY is set (any OpenAI-compatible provider), vectors are fetched at index time and search fuses scores: 0.6 × cosine + 0.4 × BM25 over a 4× candidate pool. One env var, no re-architecture. xAI does not ship an embeddings model yet; the client is pre-wired for the day it does.[Source: ...] attributions and explicitly permits "the documents don't cover this" - hallucination control by design, verifiable by the user in the UI.| Table | Purpose |
|---|---|
users | Anonymous sessions (UUID cookie, 1-year TTL, HttpOnly, SameSite=Lax) - no login required |
conversations / messages | Chat threads per user, with sources and timings stored as JSON |
chunks + chunks_fts | Retrieval store: text, metadata, optional embedding BLOB, FTS5 index |
user_sources | Per-user document/web source tracking |
logs | Structured application logs, queryable via API - observability without an ELK stack |
| Component | Choice | Notes |
|---|---|---|
| Containers | Docker Compose (2 services) | mirror (Flask/gunicorn) + caddy. V1 had 3 services and an 8 GB image; V2 image is ~450 MB |
| TLS | Caddy automatic HTTPS | Let's Encrypt with zero certificate ops; security headers (nosniff, frame-deny, referrer-policy) at the edge |
| App server | gunicorn, 2 workers × 8 threads | Requests are dominated by API wait time - threads are the right concurrency primitive |
| Persistence | Bind mounts: data/, uploads/, articles/ | Publishing an article = dropping a markdown file; it is indexed at next boot |
| Config | 12-factor, everything via .env | Model, prices, budget, keys - swappable without touching code |
Right-sizing cuts both ways: this architecture is deliberately minimal, and I know exactly where it stops being sufficient. This is the same judgment I apply on client systems - several of these "at scale" boxes are things I already run in production elsewhere.
| Dimension | Here (portfolio) | At scale (what I ship for clients) |
|---|---|---|
| Retrieval | FTS5 BM25, hybrid-ready | Qdrant/PGVector, hybrid dense+sparse, cross-encoder reranking, metadata filters |
| Inference | Grok API, budget-capped | vLLM on GPU (continuous batching, paged attention) or managed endpoints; SLO-driven autoscaling |
| Evaluation | Manual spot checks | Offline eval sets (retrieval recall, faithfulness), LLM-as-judge in CI, regression gates |
| Observability | Structured logs + SQLite log table | Prometheus/Grafana, traces per pipeline stage, cost per tenant |
| State | SQLite WAL | PostgreSQL, per-tenant isolation, migrations |
| Delivery | Compose on one VPS | Kubernetes + Helm + Terraform, blue-green rollouts (see the DevOps article) |
| Concern | Technology |
|---|---|
| Backend | Python 3.11 · Flask 3.1 · gunicorn |
| LLM | xAI Grok API (OpenAI-compatible SDK) |
| Retrieval | SQLite FTS5 (BM25) · optional API embeddings · hybrid scoring |
| Documents | PyMuPDF · python-docx · sentence-aware chunking |
| Scraping | trafilatura · BeautifulSoup fallback |
| Frontend | Vanilla JS · SSE streaming · i18n FR/EN/JA · no framework, no build step |
| Infra | Docker Compose · Caddy 2 (auto-TLS) · small VPS |
These docs are themselves indexed into the assistant's knowledge base - you can ask the AI why any of these decisions were made.