Turn a local paper source into a self-hosted web library and a continuous PDF reader.
Library list on wePaper, the maintainer’s live instance. That site is one operator’s library, not this repository.
PDF reader on the same instance: continuous scroll, in-page find, and Range fetches.
First-party sources are Zotero and a local folder of PDFs. A third adapter should not need changes to the sync engine, server, or reader.
Most paper tools give you an API, a plugin, or a hosted stack with Postgres and a worker. PaperRelay is the other shape: one process, SQLite, PDF files on disk, and a library you can actually read.
Paper source → source adapter → change probe / reconcile → sync engine
→ server (SQLite + blobs) → catalog revision → live library → continuous PDF reader
wePaper is the maintainer’s production instance of this pipeline. PaperRelay is the reusable framework. Do not treat wePaper’s papers, tokens, or host as part of the install.
| Adapter | Where it runs | What it reads |
|---|---|---|
| Zotero | Agent on the Mac/PC that runs Zotero | Local API, read-only. Collection filter. Never writes zotero.sqlite. |
| Folder | Agent on the host that has the files, or a compose profile | A directory of PDFs plus optional metadata.json / sidecar JSON. |
A missing Zotero process or an unmounted folder aborts sync. It does not unpublish the library.
Adding a third source means implementing SourceAdapter and registering it. See docs/adapters.md. Mendeley, WebDAV, and Calibre are not shipped.
Use the compose file from the GitHub Release / tag you are running. The file on main may not match a released image.
git clone --branch v0.2.0 https://github.com/rainhuang0220/PaperRelay.git
cd PaperRelay
cp .env.example .env
# set PAPERRELAY_SYNC_TOKEN to a long random string
docker compose pull
docker compose up -dThe container is the API and the UI. It does not talk to Zotero. SQLite and PDF blobs live in the Docker volume paperrelay-data at /var/lib/paperrelay. There is no Redis, Postgres, or worker.
To publish from a folder on the same host:
# in .env: PAPERRELAY_SOURCE=folder, PAPERRELAY_FOLDER_PATH=/papers, and the token
export PAPERRELAY_FOLDER_HOST_PATH=/path/to/papers
docker compose --profile folder-agent up -dWithout Docker: uv sync, build web/, then uv run paperrelay serve. Details in docs/deployment.md.
The image for this tag is ghcr.io/rainhuang0220/paperrelay:0.2.0. Use the compose file and the image tag from the same release.
The agent lives where the source lives.
# Python 3.12+
uv sync
export PAPERRELAY_SERVER_URL=http://127.0.0.1:8788
export PAPERRELAY_SYNC_TOKEN=the-same-token
# optional isolated agent state (otherwise ~/.config/paperrelay):
# export PAPERRELAY_STATE_DIR=$PWD/agent-state
# Zotero
export PAPERRELAY_SOURCE=zotero
export PAPERRELAY_ZOTERO_COLLECTIONS="My Papers"
# or Local Folder — put at least one PDF in the directory first
# export PAPERRELAY_SOURCE=folder
# export PAPERRELAY_FOLDER_PATH=/path/to/papers
uv run paperrelay doctor
uv run paperrelay daemon # automatic near-real-time sync
uv run paperrelay daemon install # optional macOS LaunchAgent
# uv run paperrelay sync --once # diagnostics onlydoctor checks the source, the token, and whether a daemon is running. sources prints the configured adapter. daemon watches the source, reconciles, and retries. An already-open catalog refreshes when the server catalog version changes. sync --once is for diagnostics, not the daily path.
See docs/sync.md for probe vs periodic reconcile, latency, and troubleshooting.
Site title, subtitle, and footer are environment variables (PAPERRELAY_SITE_TITLE, …). You do not edit React to rename the library.
An AI research assistant, a Zotero replacement, an annotation platform, or a document-management suite. The product is source → sync → serve → read.
MIT. Third-party notices, including why this tree does not vendor Zotero’s AGPL reader, are in THIRD_PARTY_NOTICES.md.

