The brain for your Paperless-ngx archive.
Chat with your documents, let a vision LLM read every page, never miss a deadline.
Installable PWA — built for mobile and desktop.
Status and expectations. This is a personal homelab project, shared as-is because it might be useful to someone else. It's alpha, it has rough edges, and I'm not a full-time maintainer — issues and PRs get answered when I have time.
I'd rather this grew into one good app than five half-finished ones, so if something is missing, broken or awkward, please open an issue or a PR here. Good work gets merged, and if you want a larger role than that, just ask. It's MIT — you're free to fork, but I'd much rather build it with you.
Paperless-ngx stores and organizes your documents. PaperlessBrain reads them — every page, through a vision LLM — and turns the archive into something you can talk to.
- Chat with your archive — an agentic tool loop that searches, reads, cross-references and cites your documents. Any Anthropic-compatible endpoint (Claude, MiniMax …) or OpenAI-compatible endpoint (local Ollama, OpenAI, OpenRouter, vLLM …) — you add models per user in Settings.
- Vision-LLM ingestion — each page is rendered to an image and read by a vision model: full-text summary, tables, actions and deadlines, extracted per document type. Runs entirely locally on Ollama if you want it to, or on any cloud vision model — same registry as the chat.
- Deadlines & actions — extracted obligations ("cancel by …", "pay until …") surfaced on the dashboard.
- Brain memory — the assistant remembers facts about you as plain Markdown files, embedded for recall in every conversation.
- Vault notes — your own Markdown knowledge base, searchable in chat alongside the archive. Works with any editor; Obsidian optional.
- Deep research — autonomous multi-step research, but the kicker is the data source: the agent researches across your own documents alongside the web. A deterministic orchestrator splits a job into sub-tasks, runs scoped agents, and synthesizes a reviewed result.
- Document generation — DIN-5008 letters (German market), email drafts, chat-to-PDF saved back into Paperless.
- Notes back into Paperless — the assistant can file a note on a document under your own Paperless account ("paid on 12.05., bank transfer"), so the record lives where the document lives.
- OCR write-back (opt-in) — after each sync, documents whose vision-read text beats what Paperless-ngx has get their text replaced there, which fixes the Paperless full-text search too. Off by default; see Text write-back.
- Email & calendar tools — per-user IMAP and CalDAV credentials, encrypted; the assistant can check mail and appointments when you ask.
- Web search — via your self-hosted SearXNG instance, with full-page reading (trafilatura, optional headless Chromium for JS-heavy pages).
- PWA — installable on phone and desktop, per-user language (English/German), dark/light theme.
This is a small project I built for my own document system. In early 2026 I tried several existing tools and none fit my needs — for what I wanted they were missing the advanced parts: using high-end consumer hardware effectively, a more detailed LLM ingestion, an information-rich vector database, and genuinely useful document detail views. It grew feature by feature and is now feature-complete enough that it feels worth sharing with the community.
My best personal results have been with Qwen3.6-35B-A3B (MTP, Q4) as the chat model — Multi-Token Prediction makes it faster than any cloud model I've tried, while staying really strong at tool use. Your mileage will vary with your own hardware and models; nothing here is tied to a specific one.
flowchart LR
P[Paperless-ngx] <-->|REST API| B(PaperlessBrain)
B <--> C[(ChromaDB<br/>+ JSON sidecars)]
B <-->|chat + vision ingest<br/>local| O[Ollama]
B <-->|chat + vision ingest<br/>optional| A["Anthropic-compatible<br/>(Claude, MiniMax …)"]
B <-->|chat + vision ingest<br/>optional| X["OpenAI-compatible<br/>(OpenAI, OpenRouter, vLLM …)"]
B <--> V[/"Vault (Markdown + git)"/]
B -->|web search| S[SearXNG]
B <-->|mail| M[IMAP]
B <-->|calendar| D[CalDAV / iCal]
Sync compares Paperless against the index, new documents are rendered page by
page and read by the vision model; results live in JSON sidecars plus ChromaDB
embeddings (multilingual-e5 — multilingual retrieval). Each run ends with
removal of deleted documents, an LLM review of the extracted deadlines, and —
if you enabled it — the text write-back into Paperless. There are two LLM
backends — anthropic and openai_compatible — and both take a custom
base_url, so any API-compatible provider works, local or cloud. They share one
tool set and one streaming event protocol, and the same model registry serves
chat, deep research and vision ingestion.
mkdir paperless-brain && cd paperless-brain
BASE=https://raw.githubusercontent.com/Vailsen/paperless-brain/main
curl -O $BASE/docker-compose.yml
curl -o .env $BASE/.env.example # fill in: PAPERLESS_URL, PAPERLESS_SUPERUSER_TOKEN, STORAGE_SECRET
docker compose up -d # pulls the prebuilt image from GHCR (no local build)Two files, ~6 KB — the app itself is the prebuilt image, so there is nothing to
clone. Prefer a fixed version over main: swap main for a tag such as
v0.2.0 in $BASE.
Before the first start, edit the vault mount in docker-compose.yml so it
points at the directory holding your existing Markdown notes — one subfolder per
Paperless-ngx user (/srv/obsidian/alice for user alice → mount
- /srv/obsidian:/mnt/vaults). Left unchanged, the app creates an empty vault
under ./vaults and your real notes are never indexed.
.env.example is the short version; grab
.env.example.full
the same way for the annotated reference with every key.
To build from source instead, clone the repository and swap image: for
build: . in docker-compose.yml.
Open http://localhost:8080 and log in with your Paperless-ngx username and
password — users, permissions and sessions come from Paperless itself.
First boot downloads the embedding model (~2.2 GB) into a Docker volume; the container needs internet access once.
Image variants: the default tag (:latest) includes headless Chromium for
JS-heavy web pages; the :lean tag is a ~1 GB smaller image where web reading
falls back to trafilatura only. Pin a release with :1.2.3 (or :1.2.3-lean)
for reproducible deploys.
| You need | Notes |
|---|---|
| Paperless-ngx | any recent version + a superuser API token |
| A vision-capable model | for document ingestion — local via Ollama (e.g. a Qwen-VL-class model, can run on another machine) or any cloud vision model (Claude, GPT, MiniMax …). Picked in Settings > Processing |
| An LLM endpoint for chat | Anthropic-compatible (Claude, MiniMax …) or OpenAI-compatible (Ollama, OpenAI, OpenRouter, vLLM …) — added per user with URL + key in Settings > AI Models |
| SearXNG | optional — enables the web-search tool |
| IMAP mailbox / CalDAV calendar | optional — enables the mail and appointment tools (per-user credentials, encrypted) |
| Wake-on-LAN capable GPU server | optional — see Power management |
Copy .env.example to .env (the short list of keys most installs need);
.env.example.full is the annotated reference with every key and its default.
Keys without a default are required. Keys
marked UI can also be changed in the app's Settings page (the app value wins;
the env value is only the initial fallback).
| Key | Required | Default | Description |
|---|---|---|---|
APP_PATH |
✅ | — | Absolute path to the app root, with trailing slash. In Docker: /app/ |
PAPERLESS_URL |
✅ | — | Base URL of your Paperless-ngx instance |
PAPERLESS_SUPERUSER_TOKEN |
✅ | — | API token of a Paperless superuser (used for sync; chat requests use each user's own session token) |
IGNORE_INBOX_TAG_AT_SYNC |
Inbox |
UI — documents carrying this tag are skipped during sync | |
EMBEDDING_MODEL |
✅ | — | Sentence-transformers model id; ships tuned for intfloat/multilingual-e5-large-instruct |
CHROMA_PATH |
✅ | — | ChromaDB directory, relative to APP_PATH (keep under data/) |
CHROMA_COLLECTION |
✅ | — | Collection name for document embeddings |
EXTRACTION_SIDECAR_PATH |
✅ | — | Directory for per-document JSON extraction sidecars |
THUMB_PATH |
✅ | — | Directory for thumbnails |
CHROMA_MAX_RESULTS |
20 |
UI — max search results per query | |
BRAIN_HINT_SIMILARITY_THRESHOLD |
0.70 |
UI — min similarity for memory hints in search | |
BRAIN_HINT_WINDOW_FACTOR |
1.5 |
UI — window factor for memory hints | |
OLLAMA_SERVER |
empty | UI — Ollama base URL for vision ingestion; also names the host the WoL/shutdown buttons control (otherwise inferred from your first local-lane model) | |
OLLAMA_INGEST_MODEL |
empty | UI — vision model used to read documents | |
EXTRACTION_PROFILE |
en |
UI — extraction-rule profile: en or de (see Extraction rules) |
|
ARCHIVE_LANGUAGE |
en |
UI — language of AI-generated summaries (archive-level — sidecars are shared by all users) | |
TZ |
system | IANA timezone for timestamps on generated documents | |
ANTHROPIC_API_KEY |
empty | UI — global fallback key for Anthropic-compatible models; every other model (and each user's own key + base URL) is configured in Settings > AI Models | |
OLLAMA_HOST_LAN_MAC_ADDRESS_WOL |
empty | MAC for Wake-on-LAN (empty = feature hidden) | |
OLLAMA_SSH_USER |
empty | SSH user for remote shutdown of the Ollama host (empty = feature hidden). Needs passwordless sudo for /usr/bin/shutdown; in Docker, mount an SSH key into the container |
|
OLLAMA_IDLE_SHUTDOWN_MINUTES |
30 |
Idle minutes before the Ollama host is shut down | |
AI_GENERATED_TAG_NAME |
AI-generated |
UI — tag applied to documents the app creates in Paperless | |
AI_GENERATED_CORRESPONDENT |
PaperlessBrain AI |
UI — correspondent for AI-generated documents | |
AI_GENERATED_DOC_TYPE |
Information |
UI — document type for AI-generated documents | |
SEARXNG_HOST |
http://localhost:8888 |
SearXNG base URL for the web-search tool | |
VAULT_ROOT |
/mnt/vaults |
Root directory holding one vault subfolder per user. In Docker this is the path inside the container — leave it at /mnt/vaults and set the host location on the left side of the volume mapping. |
|
BRAIN_SUBFOLDER |
PaperlessBrain Memory |
Vault subfolder reserved for agent-curated memory (names a real folder — change only on a fresh install) | |
VAULT_SYNC_COOLDOWN_S |
3 |
Min seconds between vault sync runs per user | |
STORAGE_SECRET |
✅ | — | Secret encrypting server-side sessions — generate with python -c "import secrets; print(secrets.token_hex(32))" |
SHUTDOWN_PASSWORD |
empty | Confirmation prompt before the shutdown button acts. Empty = no prompt, one click powers the machine off. What hides the buttons is an empty OLLAMA_HOST_LAN_MAC_ADDRESS_WOL / OLLAMA_SSH_USER |
|
HOST / PORT |
0.0.0.0 / 8080 |
Bind address and port |
The assistant's long-term memory and your personal notes live in a plain
folder of Markdown files — one subfolder per user under VAULT_ROOT:
vaults/
└── alice/
├── PaperlessBrain Memory/ ← agent-curated facts (one fact = one file)
└── ... your notes ... ← searchable knowledge base
- The folder is git-tracked by the app itself — change detection, sync bookmark and audit trail in one. You don't have to touch git.
- Obsidian is NOT required. Any editor works; the files are ordinary Markdown with a small YAML frontmatter.
- Optional topology for editing on other devices: point a WebDAV server at the same directory and sync it with Obsidian + Remotely Save. The WebDAV server must serve the same directory the app mounts — the app keeps the single authoritative copy.
Markdown on disk is the source of truth; ChromaDB is only the index and can always be rebuilt from the files.
Paperless-ngx stores whatever its OCR engine produced. The vision model usually reads the same pages better — scans, tables, poor originals — but that text only lives in the sidecars here, so Paperless' own full-text search keeps hitting the weaker OCR.
Enable Settings > Paperless-ngx write-back > "Push AI extracted text to Paperless-ngx" and every sync ends with a comparison pass: for each document whose sidecar text differs from the Paperless content, the sidecar text is PATCHed in. The comparison ignores whitespace differences, and texts under 40 characters are treated as failed extractions and never pushed.
The setting is per user and off by default, and it travels with the settings export/import.
Three things to know before switching it on:
- It overwrites. Paperless-ngx keeps no history of document text — the previous OCR result is gone. Everything else about the document (title, tags, correspondent, the file itself) is untouched.
- Paperless can overwrite it back. Reprocess, rotate, split, merge or edit a document there and its own OCR replaces the text again. The next sync pushes yours back.
- It runs with your permissions. The PATCH uses the signed-in user's own Paperless token, not the superuser token the sync otherwise uses, so it can only touch documents you may edit. Failures are counted and logged in the sync log rather than aborting the run.
It is idempotent: after a push both sides hold the same string, so the next sync finds nothing to do.
Ingestion prompts are keyed to your Paperless document type names. Because
those names are whatever you called them, the rules ship as selectable
profiles in config/extraction_rules/:
EXTRACTION_PROFILE |
Contents |
|---|---|
en (default) |
~13 common international types — Invoice, Receipt, Contract, Bank Statement, Payslip, Insurance Policy, Tax Assessment, Notice, Certificate, Letter, Report, Rental Agreement |
de |
~46 types for the German legal/administrative domain |
Any document type without its own entry falls through to _default, which still
produces usable extraction — it just lacks type-specific guidance. Nothing
breaks if your types don't match; the results are simply more generic.
To tailor it, edit the profile module and add entries keyed by your exact Paperless document-type name:
# config/extraction_rules/en.py
RULES["Warranty"] = {
"prompt": BASE_INSTRUCTIONS + """
Document type: Warranty certificate.
Pay particular attention to:
- Product, serial number and purchase date
- Warranty period and expiry date
- What is covered and what voids the warranty
""",
}To add a whole profile, drop <code>.py next to the others exporting a RULES
dict and add the code to AVAILABLE_PROFILES in __init__.py. It then appears
in the profile selector automatically.
The profile follows the names of your Paperless document types, not the
language of the documents. An English invoice filed as Rechnung still matches
the de rules — so a mixed-language archive needs no special handling.
ARCHIVE_LANGUAGE separately controls the language of generated summaries;
extracted page text always keeps the document's original language, whatever it
is. Both are set in Settings > Processing, with the .env values as
fallback.
UI ships in English and German; each user picks their language in Settings (chat answers follow it automatically). Adding a language:
- Add the code to
SUPPORTED_LANGUAGESini18n.py(e.g."fr": "Français"). pybabel init -i locales/messages.pot -d locales -l fr- Translate
locales/fr/LC_MESSAGES/messages.po. pybabel compile -d locales
For homelab GPU servers: the app can wake the Ollama host via Wake-on-LAN on
first use and shut it down over SSH after idle. Set
OLLAMA_HOST_LAN_MAC_ADDRESS_WOL and OLLAMA_SSH_USER to enable — with Docker
this needs network_mode: host (magic packets don't cross the bridge network).
The shutdown half (dashboard button and the idle watchdog) runs
ssh <OLLAMA_SSH_USER>@<ollama host> "sudo shutdown -h now" non-interactively —
no password prompt is possible, so both steps below are mandatory:
- Key-based SSH login from the app to the Ollama host. On the app host:
ssh-copy-id <OLLAMA_SSH_USER>@<ollama host>. In Docker the container has no identity of its own — mount the key read-only (see the commented line indocker-compose.yml):- /root/.ssh/id_ed25519:/root/.ssh/id_ed25519:ro - Passwordless sudo for shutdown on the Ollama host.
visudoand add:Without it the SSH call fails with<OLLAMA_SSH_USER> ALL=(ALL) NOPASSWD: /usr/bin/shutdownsudo: a password is required.
Verify without powering anything off:
ssh -o BatchMode=yes <OLLAMA_SSH_USER>@<ollama host> "sudo -n /usr/bin/shutdown --help >/dev/null && echo READY"(Inside Docker: prefix with docker exec <container> .) READY means both the
button and the idle watchdog will work.
- The superuser token is used only for sync/ingestion; every chat request runs with the logged-in user's own Paperless session token, so Paperless object permissions apply.
- Sessions are encrypted server-side with
STORAGE_SECRET. - Per-user IMAP/CalDAV credentials and API keys are stored encrypted with a key derived from the user's session — never in plaintext.
- The app is designed for LAN / reverse-proxy deployment; it does not implement rate limiting or public-internet hardening. Put it behind your proxy + SSO if you expose it.
On Windows, use Docker. The bare-metal path needs the GTK3 runtime for PDF generation, which has no pip-installable equivalent — the container ships it for you and behaves identically on every host OS.
WeasyPrint (PDF generation) loads Pango at render time, so a missing library shows up as a failed PDF export rather than a startup error. Install it and a font family up front:
# Debian/Ubuntu
sudo apt install libpango-1.0-0 libpangoft2-1.0-0 fonts-dejavu-core
# Fedora
sudo dnf install pango dejavu-sans-fonts
# Arch
sudo pacman -S pango ttf-dejavu
# macOS
brew install pangoVerify before you rely on it:
python -c "from weasyprint import HTML; HTML(string='<p>ok</p>').write_pdf(); print('PDF OK')"python3.12 -m venv .venv && source .venv/bin/activate # 3.12–3.14
pip install --extra-index-url https://download.pytorch.org/whl/cpu -e ".[crawl]"
playwright install chromium # only for the [crawl] extra
cp .env.example .env # edit values, APP_PATH = repo root
python main.pyOn GPU machines drop the --extra-index-url to get CUDA torch. A systemd unit
is the recommended way to run it as a service.
Issues and PRs welcome — this is an early alpha extracted from a personal homelab project, so expect rough edges. Please open an issue before large changes. CI runs the test suite on every PR.
NiceGUI (zauberzeug/nicegui)
is the entire frontend. Every page, dialog, the streaming chat view and the
kanban board are plain Python — no JavaScript build step, no separate frontend
service, no API layer to keep in sync. A one-person project keeps a UI this
large maintainable only because NiceGUI removed that whole category of work.
The reactive @ui.refreshable model and the async event loop shared with
FastAPI are what make token-by-token streaming into the browser a few lines
instead of a websocket protocol. Thank you, Zauberzeug.
Paperless-ngx (paperless-ngx/paperless-ngx) is the archive this is built on — it stores the documents, owns the users and permissions, and is the reason this project can concentrate on reading and reasoning instead of document management. PaperlessBrain adds to it; it does not replace it.
Also standing on: FastAPI, ChromaDB, sentence-transformers with intfloat/multilingual-e5-large-instruct, Ollama, pypdfium2, WeasyPrint and SearXNG.
MIT — use it, fork it, ship it, sell it. Attribution is the only condition.
All runtime dependencies are permissively licensed (MIT / BSD / Apache-2.0). PDF rendering uses pypdfium2 (BSD-3/Apache-2.0) and PDF generation uses WeasyPrint (BSD-3).




