Architecture

Two containers, one datastore, nothing proprietary underneath.

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.

The whole system

FactSpectra system architecture A hierarchical view. A user reaches a web UI, which calls the versioned API layer, which drives the agentic pipeline of research, draft, deterministic verification, critic and human sign-off. The pipeline reads from a single PostgreSQL instance holding vector embeddings, a keyword index, run checkpoints and audit records. An offline ingest process loads documents, chunks them with byte offsets, embeds them locally and writes to the same database. The only outbound network dependency is the model API. User UI Reference web client — ask, watch, sign off Replaceable your portal · ticketing · CLI API layer Versioned, asynchronous — runs pause for a human POST /v1/ask /runs/{id} /review /report /audit Agentic layer LangGraph pipeline — checkpointed, resumable Research agentic Draft model Verify code only Critic model Sign-off human gate Green = deterministic code, never a model. Red = a person signs. Nothing publishes until the gate is released. Model API only outbound call question + retrieved passages only retrieve Data layer One PostgreSQL 17 instance — no second datastore pgvector dense vectors tsvector keyword / BM25 checkpoints resume the gate audit + runs who signed what Hybrid retrieval fuses the first two by rank (RRF): vectors catch the paraphrase, keywords catch the exact control identifier. Offline process run once per corpus Ingest Documents mounted read-only Chunk keeps byte offsets Embed local ONNX, no network Verify offsets fails loudly Documents never leave the environment.

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.

One datastore, on purpose

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 absentWhy
Managed vector databasepgvector is sufficient and already inside the security boundary.
Cloud-specific servicesAnything AWS-only stops it running in a GCP customer’s account.
Message queueRuns are per-request and checkpointed; there is nothing to queue.
KubernetesTwo containers. An orchestrator here is cost without benefit.

Citations survive because offsets do

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.

Where the data goes

Documents

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.

Embeddings

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.

Model calls

The one external dependency. Retrieved passages and the question go to the model provider; the corpus as a whole never does.

Everything else

Chunks, vectors, checkpoints, audit records — inside your Postgres, on your infrastructure, under your backup and retention policy.

Single-tenant by construction

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.

The stack, named

Nothing exotic, and nothing proprietary. The value is in the verification discipline, not in the components.

ConcernChoice
Pipeline orchestrationLangGraph — state machine with Postgres checkpointing and interrupt_before for the human gate
ModelClaude (Anthropic Messages API) with structured outputs; refusals handled explicitly
RetrievalHybrid RAGpgvector HNSW + Postgres tsvector, fused by RRF
Embeddingsbge-small via onnxruntime, CPU, in-container — no embedding API
DatabasePostgreSQL 17 + pgvector
APIFastAPI + uvicorn, async runs, versioned under /v1
ReportsPDF, Markdown and JSON generated in-process
DeploymentDocker Compose — two containers, no orchestrator

The API is the product

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.

EndpointPurpose
POST /v1/askStart a run; returns immediately with an id.
GET /v1/runs/{id}Current stage and result.
POST /v1/runs/{id}/reviewApprove or reject. Reviewer identity recorded.
GET /v1/runs/{id}/reportThe deliverable — PDF, Markdown or JSON.
GET /v1/runs/{id}/auditThe 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.

The audit record is a first-class output

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.