DocStack
A self-hosted document pipeline: OCR, a self-hosted LLM, and an entity graph that builds and resolves itself. No document goes to a cloud API. The model runs on a borrowed GPU machine, and enrichment is paused for now.
Runs on my own hardware, plus a borrowed GPU machine for the model6 entity types with deterministic resolutionBilingual OCR, French and EnglishIncludes an MCP server for agent use
The problem
A document management system tells you where a file is. It does not tell you that three of them mention the same company under two spellings and a legal suffix.
Every product that does understand the content wants the content: uploaded, indexed, and stored somewhere else. For personal paperwork that is the wrong direction.
What it does
Documents land in Paperless, get converted to Markdown, enriched by a self-hosted LLM, and written into an Obsidian vault as a note plus an entity graph. Tags and a generated summary are written back to Paperless.
The graph is queryable: a GraphRAG endpoint answers questions with context assembled from the entities, and the code also ships an MCP server so an agent can use it as a tool.
- Ingestion
- 5-minute poll, webhook not configured yet
- Model
- Qwen3.8-27B, quantised, on a borrowed GPU
- Vault
- Obsidian, replicated by Syncthing
A model reads the documents, code decides what gets filed
Every document waits in a queue until it can be read and checked. Anything unreadable stops in plain sight, anything unsure comes to me, and a machine that is off only delays the job.
- 1Document added
- 2Job queue
- 3Readable text?(claimed)Other branch: retry: Job queueOther branch: 3 failures: Failed, visibly
- 4Model reachable?(readable)Other branch: off, backs off: Job queue
- 5Model extracts(up)
- 6Valid output?(replies)Other branch: invalid after one retry: Job queue
- 7Tags and entities(valid)Other branch: unsure: Review file
- 8Vault and graph(matched)Other branch: answers: Agent question
- 9PaperlessOther branch: Paperless down: Job queue
5 scenariosPick one, the steps above follow itPick one, the diagram replays its path
A bill signed EDF SA, from a company the graph knows as EDFFiled
- Decided by
- Code: without the legal suffix the name is the same, so it joins the existing entity.
- Model saw
- The text, the entities already known, and a shortlist of the tag vocabulary.
- You get
- One note linked to the existing EDF page, with tags and a summary back in Paperless.
A scan whose text comes out as gibberishStopped
- Decided by
- Code: under 20% of the words have four letters or more, whichever fallback produced them.
- Model saw
- Nothing. Gibberish does not make a model fail, it makes it invent a plausible document.
- You get
- After three attempts, a failed job I can see instead of a fake document in the corpus.
A document arrives while the model's machine is switched offWaits, then filed
- Decided by
- Code: an unreachable model is a wait, never a failure. The pause grows from 30 seconds to 15 minutes.
- Model saw
- Nothing until it came back, then the same input as usual.
- You get
- The same filed document, later, with nothing for me to do.
The model proposes a tag outside the vocabularyHeld for review
- Decided by
- Code: an unknown tag is not approved, and only one new proposal per document is even considered.
- Model saw
- The vocabulary shortlist, plus the right to suggest one new tag.
- You get
- The document is filed without the tag, which waits in a review file for my decision.
An agent asks what EDF billed in 2025Answered
- Decided by
- Code: it selects the documents and does the sum, and never adds two currencies together.
- Model saw
- The result and the document IDs, not an addition to work out.
- You get
- A total the agent can trace to each document behind it.
Invented examples. The real documents stay private.
Architecture
Paperless-ngx
Ingestion, OCR in French and English, and the document store. The source of truth for the file itself.
Markdown conversion
Docling first, MarkItDown as a fallback, and Paperless' own OCR text as a last resort. Three layers, because conversion is where pipelines die.
Self-hosted LLM enrichment
Ollama with a pydantic schema and forced JSON output. Structured extraction, not free text that has to be parsed afterwards.
Entity graph
A closed six-type ontology with deterministic resolution, written into an Obsidian vault and fed back to Paperless as tags. My instance runs in replace mode, so rerunning a document overwrites them.
Every stage runs on hardware I own
Under the hood
01Entity resolution is mostly not machine learning
Matching “M. Jean Dupont”, “Jean DUPONT” and “Dupont Jean” is a problem of accents, titles, and legal suffixes long before it is a problem of embeddings. The resolver normalises deterministically, uses fuzzy matching only as a tiebreak, and injects the entities it already knows into the prompt so the model stops inventing new spellings. Pairs it is unsure about go to a review file rather than being merged silently.
02A model that is switched off must not mean a failed job
The GPU node is not always reachable. Jobs whose model is unreachable wait, with backoff, and resume when it comes back. They never fail. That single decision is the difference between a pipeline that survives a power cut and one that needs a human every time.
03Only one writer may touch the tags
Two components enriching the same document will each add their own tags, and neither will be wrong. The rule is that exactly one system writes. A union mode would keep corrections made by hand, but my instance runs in replace mode, so rerunning a document overwrites them.
Gallery
Try it
This one cannot be opened up, and that is the point.
The whole premise is that the documents stay on machines I trust. Publishing an instance would contradict the only feature that matters. What is on this page instead is the pipeline, the resolution logic, and the failure modes, which is the part worth reading anyway.
Links
Nothing to open from here.
Source
Not public
Not public. It is wired to my own document store.