Skip to content

Running

Jaloliddin Ismailov edited this page Oct 28, 2026 · 4 revisions

How to run Paper Trail — on your laptop, or for real.

Locally

You need Python 3.12+, uv, and Node for the frontend. Nothing else — no cloud account, no API keys.

git clone https://github.com/unnobatroo/paper-trail
cd paper-trail
uv sync                                                # backend deps
uv run uvicorn paper_trail.api.app:app --app-dir src   # API on :8000

cd web
cp .env.local.example .env.local
npm install && npm run dev                             # UI on :3000

The tests are a good sanity check, and they're fully offline by design:

uv run pytest          # ~70 tests, no keys, no network, seconds
cd web && npm test     # 17 frontend tests

Offline vs real

By default a first real evidence search downloads the MiniLM model (~220 MB into data/models/) and queries DuckDuckGo. For fully offline work — or tests — set these in .env:

PAPER_TRAIL_EMBED_MODEL=hashing    # deterministic stub, results meaningless
PAPER_TRAIL_RERANKER=none
PAPER_TRAIL_SEARCH=fixture         # replays cached hits from data/fixtures/

With your own models

The embedder and the extraction LLM can run on any box that speaks the OpenAI API (llama.cpp, vLLM, TEI, Ollama). Point the app at it in .env:

PAPER_TRAIL_EMBED_MODEL=openai:bge-m3          # or just bge-m3
PAPER_TRAIL_EMBED_BASE_URL=http://my-box:8081/v1
PAPER_TRAIL_EMBED_API_KEY=                     # only if the server wants one

PAPER_TRAIL_LLM_BASE_URL=http://my-box:8080/v1
PAPER_TRAIL_LLM_API_KEY=local
PAPER_TRAIL_LLM_MODEL=qwen2.5-vl-7b-instruct   # llama.cpp ignores the name
PAPER_TRAIL_LLM_WINDOW=1                       # pages per call

Things to know:

  • Vectors are cached per model name (openai:bge-m3), so switching models means re-embedding: clear the fetched/ cache or the old model's chunks rows. The same goes for pointing the base URL at a server with different weights under the same model name.
  • The pgvector column is vector(1024). Shorter vectors are padded, wider ones fail loudly on write, so pick a model with 1024 dims or fewer (BGE-M3 is exactly 1024).
  • The default window is 4 pages per LLM call. A small model on a shared box may time out on that; PAPER_TRAIL_LLM_WINDOW=1 sends one page per call. If a multi-page call times out, the extractor retries that window page by page.
  • Reranking stays off (PAPER_TRAIL_RERANKER=none) unless you also configure one, so matches are ranked by text similarity only.

With Supabase

Run the files in supabase/migrations/ in the SQL editor (001 through 006, in order), put SUPABASE_URL and SUPABASE_KEY (service role) in .env, and push the seed PDFs once with uv run python scripts/push_data.py. From then on, all state — rows, fetched pages, embeddings, documents, jobs — lives in Supabase and nothing is stored in the repo or on local disk.

Going the other way, uv run python scripts/pull_data.py mirrors the source PDFs, the benchmark files, and the official_sources registry into the local SQLite — so an offline dev run searches the same catalogue as production.

Deployed

The live instance runs on the cheapest thing that works:

  • Frontend — Vercel, root directory web/, one env var (NEXT_PUBLIC_API_URL → the API). Pushes to main deploy automatically. NEXT_PUBLIC_REVIEW_KEY exists but is for private deployments only — it ships in the JS bundle.
  • API — Azure App Service on the free tier, deployed as a zip. The durable jobs table is what makes this viable: if the instance restarts mid-job, the sweeper marks it and it can be retried. Set PAPER_TRAIL_REVIEW_KEY here if strangers shouldn't be able to click Confirm — the UI then asks reviewers for it via a dialog in the top bar.
  • Data — Supabase (free tier): Postgres, pgvector, Storage, jobs table.

The API's Dockerfile also works on any container host (Render, Fly, Railway) — same env vars, same behaviour. The only thing to remember is PAPER_TRAIL_MODEL_CACHE: point it at persistent disk (/home/data/models on App Service) or the model re-downloads on every restart.

One caveat worth knowing: MiniLM on 1 GB of RAM is workable but snug. Keep job concurrency low (the runner ships capped at two workers) and don't enable the local reranker — it's another ~1 GB. If quality ever justifies the cost, drop in a JINA_API_KEY and the default auto mode picks up the hosted reranker on the next restart; PAPER_TRAIL_EMBED_MODEL=jina moves embeddings up there too, and the F1 box barely notices either way.

Housekeeping

  • scripts/reset_demo_state.py — wipes domain data, keeps caches, so a fresh demo doesn't re-fetch or re-embed anything.
  • requirements.txt is generated for the container build — regenerate after dependency changes: uv export --format requirements-txt --no-hashes --no-dev --no-group ml

Clone this wiki locally