Dokki: a RAG pipeline in three planes
Documents in, grounded answers with sources out — split into a data, control, and query plane on the existing MJTHD stack.
Dokki is a RAG pipeline: documents — PDFs, eBooks, internal notes — go in, and out come LLM answers that are grounded against exactly those sources and cite them. No hallucinating about knowledge that was never in the corpus.
The interesting part isn't the LLM, it's everything around it: how does a document reliably reach a searchable, versioned state — and how do you prevent a broken import from poisoning production? Dokki answers that with three NestJS microservices, cut by responsibility.
Three services, three planes
| Service | Plane | State | Responsibility |
|---|---|---|---|
| Ingest | Data Plane | stateless, scalable | document → blob → hash → parse → chunk → embed → vectors |
| Knowledge | Control Plane | Single Source of Truth | registry, dedup, versions, snapshots, quality gate, promotion/rollback |
| RAG / Inference | Query Plane | stateless | question → embed → top-k from active snapshot → prompt → answer with sources |
The separation is the central design decision. The ingest only produces chunks and numbers — it never decides whether a result is "good". That decision belongs to the Knowledge Service alone. And the RAG service is pure read side: it only ever queries the active state.
How it fits together
Decoupling runs over NATS JetStream: the ingest worker listens on
ingest.requested, reports back ingest.completed, and a successful promotion
emits snapshot.activated, upon which the RAG service invalidates its cache.
Each collection (a bounded knowledge corpus) gets its own subject namespace with
its own token.
The stack
Dokki invents no infrastructure; it builds on what already runs in the MJTHD cluster:
- CNPG / pgvector — vectors and metadata, HNSW index on
halfvec - NATS JetStream — event bus, 3-replica, per-collection token
- MinIO — original blobs, content-addressed by their hash
- Ollama (
bge-m3) — embeddings, as a shared deployment for ingest and RAG - Claude — the answer LLM, with the native Citations API for sources
- k3s + ArgoCD — GitOps deployment like the rest of the stack
Anthropic has no embeddings API — so Ollama handles text-to-vector, and Claude only answer generation. Two different jobs, two different models.
The one idea that carries everything
Re-ingest never produces an update, always a new immutable version. A state goes live only through active promotion.
Almost the entire rest of the architecture follows from this: versions, snapshots, rollback as a mere pointer flip, and a quality gate that keeps bad states from going live at all. The next posts in this series take exactly these pieces apart — starting with why a bad ingest can't break Dokki.