PRKS is a self-hosted, local-first research library for organizing PDFs, Markdown research notes, online video references, bibliographic metadata, people, tags, reading progress, PDF annotations, playlists, and structured research relationships.
License: GNU Affero General Public License v3.0 only (AGPL-3.0-only). See Licensing for third-party notices and source-availability details.
It uses Python, SQLite, and on-disk files on your machine—no separate database server is required.
- Organize research with Works, Folders, Tags, People/Roles, Playlists, progress, and Saved Views.
- Read and annotate managed PDFs inside PRKS.
- Keep Research Notes alongside source material and connect them to Concepts, Positions, Arguments/Stances, and the Research Graph.
- Work across in-app tabs and split panes without opening a collection of browser tabs.
- Use progressively local-first/offline-capable workflows with durable browser-side operations for supported domains.
- Back up and restore the canonical PRKS library through the application.
- Run locally with Python/SQLite or with Docker Compose.
- Python 3.12+
- Exact runtime package pins from
requirements.txt
Prefer a project-local virtual environment. On PEP 668 “externally managed” systems, do not use sudo pip or --break-system-packages.
Prefer a project-local virtual environment (required on PEP 668 “externally managed” systems — never use sudo pip or --break-system-packages):
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
./.venv/bin/python prks_app.pyprks_app.py validates the Python environment (minimum version + exact requirements.txt pins via importlib.metadata) before DB recovery, migrations, storage binding, or server startup. A mismatch exits non-zero with platform-aware remediation. It never auto-runs pip and never contacts PyPI.
The process listens on 127.0.0.1:8080 only. Open http://127.0.0.1:8080 in a browser. No extra firewall or network setup is required for this case.
Optional port (still loopback):
python prks_app.py --port 9000To listen on every local interface (LAN or VPN), pass an explicit host:
python prks_app.py --host 0.0.0.0--host localhost also works and binds that name. The default is the literal address 127.0.0.1, not localhost.
PRKS has no application-level authentication. Do not expose it to an untrusted network. See Security and Operations before changing the bind/publish address.
python prks_app.py --testingThis sets PRKS_TESTING=1 and uses port 8070 by default (unless you pass --port). With PRKS_STORAGE unset it defaults to data_testing/ so repo data/ is untouched. You may set PRKS_STORAGE to an explicit safe testing root. Testing mode refuses /data and the repository data/ directory (and descendants), including via symlinks. prks_app.py is the only process entry.
Build with ./docker-build.sh, which builds prks:latest and prunes dangling images from previous builds. Then run Compose from the repo root:
docker compose up -dDocker images build the classic frontend into two ordered JavaScript bundles (rather than 84 separate requests); the raw Python source checkout remains unbundled. To rebuild the previous frontend without bundles, use docker compose build --build-arg PRKS_CLASSIC_BUNDLES=0 followed by docker compose up -d. This affects packaged static assets only, not library files or the database. Build details, cache versioning and validation: Classic-script distribution.
The container process binds 0.0.0.0:8080 so Docker port forwarding can reach it. Compose then publishes that port on the host loopback only (127.0.0.1:8080:8080). Container 0.0.0.0 is not the same as exposing PRKS on every host interface.
Open http://127.0.0.1:8080 on the machine that runs Compose. This also sets PRKS_STORAGE=/data, mounts ./data on the host to /data in the container, and runs the process as ${UID:-1000}:${GID:-1000} so files on the bind mount match your user. The entrypoint creates /data/pdfs if needed and runs python /app/prks_app.py --host 0.0.0.0.
To publish the host port on every interface (LAN access):
PRKS_PUBLISH_HOST=0.0.0.0 docker compose up -dUse that override only on a network you already trust, or behind an access layer you control.
docker-compose.postgres.yml runs PRKS as the PostgreSQL design lays it out (deployment issue #630, docs/postgresql-architecture.md §12.3). It is a preview: PostgreSQL is not the production backend until the cutover. Accounts, sign-in, the database/data-root pairing and background jobs live in PostgreSQL, while the Library's content is still the SQLite store in the data root. Plain docker compose up -d keeps running the SQLite application above. Use the PostgreSQL file on its own, never together with docker-compose.yml: both would serve the same ./data.
Four services:
| Service | What it does |
|---|---|
db |
PostgreSQL 18; named volume pgdata at /var/lib/postgresql (the PostgreSQL 18 location, never /var/lib/postgresql/data); no host port |
migrate |
One-shot: applies the schema, then, once setup is complete, runs the start check (below) and exits. web and worker start only after it succeeded |
web |
prks_app.py serve with password sign-in (PRKS_AUTH=session); healthcheck on /readyz |
worker |
prks_app.py worker: background jobs, beside web |
Every service sets PRKS_MIGRATE_ON_START=0, so migrate is the only schema runner, and reads the database URL from the prks_database_url Compose secret (PRKS_DATABASE_URL_FILE). Only the services that list a secret can read it.
1. Secrets and data directory. Create them once, owner-only, outside version control (secrets/ and .env are ignored by Git and by Docker builds):
install -d -m 700 secrets data
umask 077
# Refuse an existing file: PostgreSQL reads the password only when it creates the database.
( set -C; openssl rand -hex 32 > secrets/db_password ) || exit 1
printf 'postgresql://prks:%s@db:5432/prks\n' "$(cat secrets/db_password)" > secrets/database_url
cp .env.example .env # then set UID/GID to `id -u`/`id -g`Do not regenerate secrets/db_password on an initialized volume: changing the file does not change the prks role's password. Rotate it with an authenticated ALTER ROLE and update both secret files together.
2. Start and set up.
docker compose -f docker-compose.postgres.yml build
docker compose -f docker-compose.postgres.yml up -dOn a new, empty data directory web binds it as a PRKS data root (prks-root.json) and then waits for setup: it serves nothing, and /readyz stays unreachable (the container is unhealthy) until you create the owner and the Library. worker waits too. Run setup in the waiting web container (admin bootstrap):
docker compose -f docker-compose.postgres.yml exec web python /app/prks_app.py admin bootstrap issue-token --root /data
docker compose -f docker-compose.postgres.yml exec web python /app/prks_app.py admin bootstrap complete --root /data --login aliceweb then starts serving at http://127.0.0.1:8080 and turns healthy. Loopback is the default publish address. Expose PRKS on a network only behind an HTTPS reverse proxy, with PRKS_PUBLISH_HOST, PRKS_PUBLIC_ORIGIN and PRKS_TRUSTED_PROXIES set in .env. PRKS_TRUSTED_PROXIES must name only the proxy, and the web port must then be reachable only by that proxy (keep it on loopback, or drop the published port and put the proxy on the Compose network). Anyone who reaches the port from a trusted address can set X-Forwarded-For and spread their sign-in attempts over invented addresses.
Health and stopping. GET /healthz answers 200 while the process runs. GET /readyz answers 200 only when the database is reachable, its schema is exactly this image's, setup is complete and the data root is still paired with this database; otherwise 503. Neither shows any detail. On docker compose stop (SIGTERM) web stops accepting connections and exits, and worker claims nothing more and finishes its running job or hands it back, within stop_grace_period (30 s).
Start generation. The database and the data root's prks-root.json both record a start generation. Every start of migrate, web or worker checks that the two match and moves both forward by one, database first. A database behind its data root, or one that ran elsewhere, is refused. If a start stops between its two writes, web stays not ready until the next start of any service finishes it (docker compose -f docker-compose.postgres.yml run --rm migrate does). The start generation counts starts, not writes, so it cannot catch every stale copy of the database (below).
Upgrades. Build the new image, then docker compose -f docker-compose.postgres.yml up -d: migrate runs first, while the previous web and worker may still hold the data root, and the new web and worker refuse a schema other than their own. Before upgrading PostgreSQL itself or moving its volume, read the next two paragraphs.
One database per data root. At most one database ever runs against a data root. Two databases running against one data root at the same time can permanently delete files, and PRKS cannot detect it: each database's cleanup removes files that only the other one still references. A copy of the database (a volume snapshot, pg_dump, an upgraded or moved cluster) looks like the original to PRKS:
- A database copy taken while PRKS is serving is never started against this data root. Start it only against its own copy of the data root, taken at the same moment (whole-instance disaster recovery).
- Once a copy (for example an upgraded cluster) has started against the data root, retire the database it came from: move the old cluster's data directory out of the volume, or delete it. It must never start here again.
- Reverting the image tag is not a rollback. Prepare a rollback before upgrading. Either keep the old database's data directory together with a copy of the data root, both taken offline at the same moment, and restore them together. Or restore a format-2 Library backup into a fresh instance with its own new data root, never into the old database on this root.
Database volume capacity. PRKS cannot see the free space on the PostgreSQL volume. Monitor it, and keep free at least twice the database's size plus max_wal_size (1 GB by default): a Library restore stages a copy of the Library's rows before the old ones are vacuumed. If the volume fills up, free space, restart db if it stopped, and let PRKS clean up the failed restore on its next start. A recreated (empty) database volume does not take over a used data root: setup refuses it, and its files stay untouched.
Lifecycle. docker compose -f docker-compose.postgres.yml down stops every service and keeps the database volume. down -v deletes the database volume. Always pass -f docker-compose.postgres.yml. The CI acceptance run of this file is scripts/ci/test_postgres_compose.sh.
| Variable | Purpose |
|---|---|
PRKS_STORAGE |
If set, root directory for persistent data. Database: $PRKS_STORAGE/prks_data.db. PDFs: $PRKS_STORAGE/pdfs/. Thumbnails: $PRKS_STORAGE/thumbs/. |
PRKS_TESTING |
When truthy (1, true, yes), uses testing paths and stricter checks (see Testing mode above). |
PRKS_THUMB_LOSSLESS |
When truthy, PDF card thumbnails use lossless WebP/PNG cache encoding (debugging). Default is card-optimized lossy WebP; cache filenames use rev _v2. |
PRKS_LOG_LEVEL |
Stderr log level. Default INFO. Changes volume, not what kinds of data may be logged. |
PRKS_LOG_FILE_LEVEL |
Persistent file log level. Default ERROR. |
PRKS_LOG_RETENTION_DAYS |
Rotated persistent log copies to keep. Default 7. |
PRKS_LOG_FILE |
Override path for the rotating error log. Default $PRKS_STORAGE/prks-errors.log. |
PRKS_PERF_SLOW_MS |
API request duration in milliseconds counted as slow. Default 250. Clamped to 10–60000. Does not timeout requests. |
PRKS_PERF_LOG_SLOW |
When truthy (1, true, yes, on), emit a privacy-safe slow_request INFO log for slow API requests. Default off. |
PRKS_BACKUP_MAX_UPLOAD_BYTES |
Maximum size of an uploaded .prks-backup restore archive. Default 64 GiB. A valid Content-Length is required. |
PRKS_CONFIG_FILE |
Override path of the bootstrap configuration file (default: ~/.config/prks/config.json on Linux, ~/Library/Application Support/PRKS/config.json on macOS, %LOCALAPPDATA%\PRKS\config.json on Windows). Ignored in testing mode. |
If PRKS_STORAGE is unset, non-testing runs use the project’s data/ directory: data/prks_data.db, data/pdfs/, data/thumbs/, and person portrait cache data/people/ (lossy WebP, max 512px edge, keyed by person id + image_url hash).
The storage root is chosen by the first of these that is set, and a set but invalid choice is a startup error rather than a fall-through: python prks_app.py --storage-root PATH (this run only), PRKS_STORAGE, storage.local_root in the bootstrap configuration file (nothing in PRKS writes that file yet), then the default above. Testing mode never reads the bootstrap file. A relative or ~ path is made absolute once at startup.
Every storage root carries a small prks-root.json marker with a stable storage_root_id. PRKS writes it the first time it opens a root: a new empty directory, or an existing library (one that has prks_data.db or pdfs/). PRKS refuses to open a non-empty directory that does not look like a PRKS library, a root whose marker is unreadable or from a newer PRKS, a root a storage move left retired or unfinished, and a root selected by the bootstrap file that is missing or empty (for example an unmounted disk). It also refuses links inside the root (the root itself may be a link), components on another filesystem, and roots on NFS/SMB and other network filesystems. The marker is not part of backups; a restore keeps the target root's marker.
Exactly one PRKS server process may use a storage root at a time. The running process holds an operating-system lock on .prks-maintenance/root.lock until it exits, and a second process pointed at the same root stops with a "storage root is already open" error.
Person profile images (GET /api/persons/{id}/profile-image) are optional. image_url must be a direct public HTTP/HTTPS URL (HTTPS preferred) that itself returns HTTP 200. PRKS does not follow redirects, and private/local/link-local targets are refused. Only static JPEG/PNG/WebP/GIF rasters are accepted. The download is size- and time-bounded; the image is decoded and transcoded (max 512px edge, usually WebP) before anything is cached. Original remote bytes are not kept. Local portrait upload is not part of this feature. Updating a valid image_url clears that person’s cached portraits.
PRKS automatically upgrades supported older databases at startup.
Current schema version: 17.
Schema migrations are transactional and version-ordered. A database marked version N has passed every migration through N.
Databases created by a newer PRKS version are refused rather than downgraded. Download a verified backup before installing a PRKS revision that announces a database schema upgrade.
Exact startup, configuration, and safety contracts stay in this README (host/port, Docker publish, env vars, schema version, auth warning). Detailed current feature and user behavior lives in the version-controlled Wiki source, which is published to the GitHub Wiki.
| Topic | Documentation |
|---|---|
| Install and run | Getting Started |
| Everyday workflows | User Guide |
| Tabs and split view | Workspace Tabs and Split View |
| PDFs and annotations | PDFs and Annotations |
| Research graph/model | Research Network |
| Offline/local-first | Offline and Sync |
| Storage and backups | Storage, Backup, and Restore |
| Configuration/logging/performance | Configuration and Operations |
| Troubleshooting | Troubleshooting |
| Architecture | Architecture |
| Contributor commands | Developer Reference |
| Dependencies/vendoring | Dependencies and Vendoring |
| Security | Security and Operations |
Implementation/design authorities that intentionally remain outside the Wiki:
- AGENTS.md — contributor/agent architecture and testing rules.
- DESIGN.md — authoritative visual/interaction contract.
- SECURITY.md — vulnerability reporting.
- docs/local-first-sync.md — detailed sync design.
- docs/local-first-rollout-status.md — current local-first coverage.
Normal runs use data/ unless PRKS_STORAGE selects another storage root. The canonical library includes the SQLite database and managed research files; several indexes and caches are derived and rebuildable.
Use Settings → Backup & restore → Download backup for supported backups. A .prks-backup contains private research data and should be protected like the live library.
See Storage, Backup, and Restore before manual recovery, moving storage, or restoring a library.
- Command palette — Ctrl/Cmd+K for navigation, search, and common creates. Details: User Guide.
- Workspace tabs / split view — in-app tabs, Main + Secondary panes, resize, and local layout memory. Details: Workspace Tabs and Split View.
- Research Graph — read-only map of Concepts, Positions, Arguments/Stances, and note references. Details: Research Network.
- Saved Views — store search definitions, not result snapshots. Details: User Guide.
Install runtime pins plus development/test tooling (includes openapi-core for the unit-contract preflight that python run_tests.py always runs):
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt -r requirements-dev.txtFast test suite (no Chromium):
./.venv/bin/python run_tests.pyFull browser E2E gate (Playwright; Chromium installs into .playwright-browsers/ if missing):
Every E2E server runs the PostgreSQL-backed sign-in on its own disposable database, so E2E needs PRKS_TEST_DATABASE_URL set to the postgres maintenance database of a disposable PostgreSQL 18 server (the same variable scripts/test-postgres uses). Without it, each E2E server refuses to start.
./.venv/bin/python run_tests.py --e2eA runtime-only install (requirements.txt alone) is enough to run prks_app.py, but not enough for run_tests.py: the unit suite preflights openapi-core from requirements-dev.txt.
Use Developer Reference for targeted E2E modes, UX tours, browser fixtures, Sonar tooling, project layout, and the local post-commit archive. Contributor/agent rules are authoritative in AGENTS.md.
Copyright © 2026 Nikola Perović and other PRKS contributors. Each contributor retains copyright in their own contributions. This notice does not claim ownership of vendored, third-party or user-supplied material.
PRKS's original application code and documentation are made available under the
GNU Affero General Public License version 3 only
(SPDX-License-Identifier: AGPL-3.0-only), subject to the rights of
individual authors and separately licensed materials. You may use, modify, and
redistribute covered code under that license; no warranty is provided.
This license does not relicense third-party dependencies, vendored scripts, fonts, PDF viewer components, or user-supplied research files. The third-party notices list bundled components and their original license files. Contributors should read CONTRIBUTING.md.
Source for network users: The unmodified upstream source is at Fooftilly/PRKS. The application also exposes a source and license link under Settings → Diagnostics → About PRKS. If you distribute or operate a modified PRKS, you are responsible for making the Corresponding Source for your actual deployed version available under the applicable AGPL terms. An upstream link alone is not sufficient for your own modifications; update the in-app source link or supply an equally prominent link to the exact matching source, including build instructions and required corresponding material. The application's original data (PDFs, notes, research libraries, and backups) is not licensed by this project.
PRKS is a single-user application without built-in authentication. Loopback is the safe default. Network exposure, trusted host behavior, browser-origin protections, Markdown sanitization, privacy-safe logging, and remote portrait constraints are documented in Security and Operations.
For vulnerability reporting, see SECURITY.md.


