I'm currently open to new opportunities and looking for a job.
Senior full-stack developer with 6 years of experience, strongest in React and Node.js/NestJS, used to owning a feature from the schema through to the screen.
A self-hosted, bring-your-own-keys AI companion for paperless-ngx: better OCR and automatic titles, tags, correspondents and dates, configured entirely from a web UI.
![]() |
![]() |
![]() |
![]() |
- Monorepo: Turborepo + pnpm
- Backend: NestJS (HTTP API + cron poller + in-process worker), SQLite via Drizzle, self-written table queue
- Frontend: React + Vite + TanStack Query + Tailwind
- AI gateway: Vercel AI SDK (structured output via Zod)
apps/api NestJS: API + poller + worker, one process (also serves the built SPA)
apps/web React + Vite admin SPA
packages/shared Zod schemas, types, constants (FE/BE contract)
- You tag a document in paperless-ngx with
psf-process. - The poller picks it up, OCRs it (optional) and asks an LLM for a title, tags, correspondent and date.
- Depending on the Auto-apply setting, the result is either written straight back to paperless (and the trigger tag removed), or queued in the Review screen for you to approve/edit/reject.
Everything is configured in the web UI: the paperless connection, provider keys, models, prompts, page limits, the polling interval. The only thing set outside the UI is a handful of environment variables for the process itself (below).
A single container, no Postgres/Redis. SQLite lives on a mounted volume. Pre-built images are published to GitHub Container Registry on every GitHub release (the release tag names the image version), so there is nothing to clone or build:
ghcr.io/sabbaken/paperless-starfruit/api:latest # or a release version, e.g. :1.1.0, to pin
Two compose files, depending on where you start (full walkthrough in the installation docs):
Use docker/docker-compose.yml (just the Starfruit container).
Put the one required secret in a .env next to it and start:
# Keep ENCRYPTION_KEY stable (see below).
echo "ENCRYPTION_KEY=$(openssl rand -base64 32)" > .env
docker compose up -dTo let the container reach paperless, attach it to your paperless stack's Docker network
(see the commented example at the bottom of the compose file) and use the in-network service
name (e.g. http://webserver:8000) as the paperless URL in the UI, not localhost.
Use docker/docker-compose.full.yml: Redis +
paperless-ngx + Paperless Starfruit together. This one needs two secrets:
cat > .env <<EOF
ENCRYPTION_KEY=$(openssl rand -base64 32)
PAPERLESS_SECRET_KEY=$(openssl rand -base64 32)
EOF
docker compose -f docker-compose.full.yml up -d
docker compose -f docker-compose.full.yml run --rm paperless createsuperuserpaperless-ngx comes up on http://localhost:8000 (log in with the superuser you just
created). In the Starfruit UI, set the paperless URL to http://paperless:8000 (the
in-network service name, not localhost).
Open http://localhost:7827. The container serves both the API and the web UI on
the same port. (The compose files publish the app on host port 7827, deliberately
uncommon so they work as-is; change the left side of the ports: mapping to move it.)
On first run, the UI shows a "create admin" card; once you set the admin
username/password, registration closes and only login works (there is no default password).
- Persistence: the SQLite database is at
/data/app.dbon a named volume. Back that up to keep your config, prompts, queue and audit history. - Automatic updates: both compose files ship a Watchtower service that auto-updates the
Starfruit container when a new image is released (only the labelled container, nothing else
on the host). Remove the
watchtowerservice, or pin a version tag instead of:latest, to upgrade deliberately. - Behind a reverse proxy: put your TLS terminator (Caddy/Traefik/nginx) in front of the
published port (
7827by default). Auth is a bearer token, not a cookie, so no extra CORS/cookie config is needed; setCORS_ORIGINonly if you want to restrict it. - Building from source (optional): clone the repo, swap
image:for the commentedbuild:block indocker/docker-compose.yml, thendocker compose -f docker/docker-compose.yml up -d --build.
These four are the only runtime env vars. Everything else is configured in the UI.
| Variable | Required | Default | Purpose |
|---|---|---|---|
ENCRYPTION_KEY |
yes | (none) | 32-byte key (base64 or hex). Encrypts stored credentials and signs admin sessions. |
DATABASE_PATH |
no | /data/app.db |
SQLite file path (its directory is created if missing). |
PORT |
no | 3000 |
Port the app listens on inside the container (the compose files publish it as 7827). |
CORS_ORIGIN |
no | reflect origin | Comma-separated allowed origins. Leave unset unless you want to lock it down. |
Keep
ENCRYPTION_KEYstable. Rotating it logs the admin out and makes every stored paperless/provider credential undecryptable. You'd have to re-enter them. Store it like any other production secret.
Configured in the UI (not env): the paperless URL + token, AI provider keys, model choices, prompts, page limits, the polling interval, auto-apply, and the correspondent blacklist. Worker concurrency is fixed at 1 by design (single-user scale: simplicity over throughput).
In the UI's Connection screen, enter your paperless base URL and an API token, and hit Test connection. Leave API version blank; it's auto-detected.
Prefer a dedicated paperless user over the superuser: create a paperless account that can view and change documents and view and add tags and correspondents, then mint that user's token (paperless: Settings → My Profile → API Auth Token). The token is stored encrypted at rest. Because this app must reach self-hosted/LAN URLs (paperless on a private IP, a local Ollama, etc.), the admin-entered URLs are intentionally not SSRF-restricted: the security model is "one trusted admin".
Add provider credentials in the API Keys screen. Supported kinds:
- Anthropic, OpenAI, Google, Mistral: one API key each; models come from a curated catalog.
- OpenAI-compatible: for local/self-hosted endpoints (Ollama, LM Studio, vLLM, OpenRouter); set a base URL, the API key is optional, and models are discovered live.
Keys are encrypted at rest (AES-GCM) and never returned to the browser unmasked. Pick the default LLM and OCR models in the Processing screen.
pnpm install
cp .env.example .env # then set ENCRYPTION_KEY (see the comment in the file)
pnpm dev # run api (:3000) + web (:5173) togetherOpen http://localhost:5173. The web dev server proxies /api to the API on port 3000.
(In production the API serves the built SPA itself on the same port.)
- Migrations apply automatically on API boot, so there's no separate migrate step for a normal
run.
pnpm db:generate(regenerate a migration after a schema change) andpnpm db:migrate(apply migrations standalone, e.g. in CI) remain available. ENCRYPTION_KEYis required to save credentials. The API reads.envfromapps/api/or the repo root.
A throwaway paperless-ngx instance to develop against (SQLite + Redis, default
admin / admin login, dev only):
pnpm paperless:up # start paperless on http://localhost:8000 (first boot ~30-60s)
pnpm paperless:token # print an API token for the admin user
pnpm paperless:logs # tail paperless logs
pnpm paperless:down # stop itThen in the onboarding screen use http://localhost:8000 and the token. Drop PDFs/images into
docker/paperless-consume/ to have paperless ingest them, and tag one psf-process to test.
pnpm buildCopyright (C) 2026 Kiryl Viarenich.
Paperless Starfruit is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your option)
any later version. It is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the LICENSE file for the
full text.



