Architecture
Most of the design decisions here were made so the same artifact can run on a laptop and inside a customer’s VPC without a second implementation. That constraint removes options, and the ones it removes are mostly the ones that cause trouble later.
That is the entire deployment. The same Compose file runs on a developer laptop and on a production host — cloud portability demonstrated by doing it twice, not asserted on a slide.
Postgres carries all four jobs: dense vectors via pgvector,
keyword search via its own full-text index, the pipeline’s checkpoints, and
the audit records.
No separate vector database, no Redis, no queue. Every additional datastore is another thing a customer’s platform team must provision, secure, back up, and be paged about — and none of them buys anything at this scale.
| Deliberately absent | Why |
|---|---|
| Managed vector database | pgvector is sufficient and already inside the security boundary. |
| Cloud-specific services | Anything AWS-only stops it running in a GCP customer’s account. |
| Message queue | Runs are per-request and checkpointed; there is nothing to queue. |
| Kubernetes | Two containers. An orchestrator here is cost without benefit. |
A conventional chunker returns text and discards where it came from. That is fine when the answer is a summary and useless when a claim must point at a sentence.
FactSpectra keeps character offsets through the whole path, so a retrieved passage can be narrowed to the exact clause quoted:
document_text[chunk.start:chunk.end] == chunk.text # invariant, tested
chunk (1,391 chars, p.23)
└─ span 49,179–49,219 → "authorized network communication"
The extracted text is stored alongside the document, because PDF extraction is lossy and not reversible — offsets into the original file would be meaningless, and a citation you cannot re-verify later is decoration.
Ingest fails loudly if any offset does not round-trip. Storing citations that point at the wrong text would be worse than storing none: they look trustworthy.
Mounted read-only at runtime. Never copied into the container image, so a built image can be shipped without carrying anyone’s content inside it.
Generated by a model running in your container — ONNX, CPU, no network. Indexing a pen-test report does not send it to a third-party embedding API.
The one external dependency. Retrieved passages and the question go to the model provider; the corpus as a whole never does.
Chunks, vectors, checkpoints, audit records — inside your Postgres, on your infrastructure, under your backup and retention policy.
Because each deployment lives in one customer’s environment, there is no
shared multi-tenant database, no tenant_id on every query,
and no cross-tenant leakage to reason about. The isolation is
architectural rather than enforced by application code that has to be
right every time.
Nothing exotic, and nothing proprietary. The value is in the verification discipline, not in the components.
| Concern | Choice |
|---|---|
| Pipeline orchestration | LangGraph — state machine with Postgres checkpointing and interrupt_before for the human gate |
| Model | Claude (Anthropic Messages API) with structured outputs; refusals handled explicitly |
| Retrieval | Hybrid RAG — pgvector HNSW + Postgres tsvector, fused by RRF |
| Embeddings | bge-small via onnxruntime, CPU, in-container — no embedding API |
| Database | PostgreSQL 17 + pgvector |
| API | FastAPI + uvicorn, async runs, versioned under /v1 |
| Reports | PDF, Markdown and JSON generated in-process |
| Deployment | Docker Compose — two containers, no orchestrator |
The web interface is a reference client. Everything it does, your systems can do — that is the point, because your reviewers work in your tools, not ours.
| Endpoint | Purpose |
|---|---|
POST /v1/ask | Start a run; returns immediately with an id. |
GET /v1/runs/{id} | Current stage and result. |
POST /v1/runs/{id}/review | Approve or reject. Reviewer identity recorded. |
GET /v1/runs/{id}/report | The deliverable — PDF, Markdown or JSON. |
GET /v1/runs/{id}/audit | The full chain, including what was rejected. |
Runs are asynchronous because they pause for a human. A synchronous request that blocks until someone approves something is not a design.
Not a log line. A retrievable record of every run: the searches the model chose, the passages retrieved, the draft it proposed, the quotes that were rejected and why, what the critic flagged, who approved it and when.
When someone asks “what was this answer based on, and who signed it?”, that endpoint is the answer. It deliberately includes what was thrown away — a trail showing only survivors is a summary, not evidence.