Papyrd is an eBook server that implements OPDS for eBook discovery and download as well as Kosync for reading progress sync. Right now it is a minimal implementation that implements basic eBook uploading and the minimal work needed for Kosync and OPDS to work.
The goals of this server are to be as compatible as possible with open source protocols. Any compatability issues with clients that correctly implement OPDS and/or Kosync should work with this server and any compatability issues will be investigated as a bug.
That said I am also developing an official companion app at Papyrd Mobile. Check out that project if you want a minimal and simple ereader app for iOS or Android.
This project is in alpha. I feel very confident that the storage model and other database related things are in a good spot and will not require any breaking changes. But standard self hosted rules should absolutely apply. Make regular backups.
As AI is a controversial topic in the self hosting community. I feel the need to disclose that I make regular use of AI tools in my development. I do not vibe code and review any AI generated code before committing.
This docker compose example should get the app running but feel free to modify for the specifics of your homelab setup.
services:
papyrd:
image: ghcr.io/rileymathews/papyrd-server:latest # or tagged release
restart: unless-stopped
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://papyrd:change-me@postgres:5432/papyrd
PAPYRD_SESSION_SECRET: change-this-to-a-long-random-string
PAPYRD_BIND_ADDRESS: 0.0.0.0:3000
PAPYRD_STORAGE_ROOT: /app/storage # THIS MUST MATCH INGEST STORAGE ROOT IF USING INGEST SERVICE
PAPYRD_DISABLE_SIGNUP_AFTER_FIRST_USER: true # disables signup after the first user has registered. See notes on server config below for more info
PAPYRD_INVITE_EXPIRATION_SECONDS: 86400 # optional, defaults to 1 day
volumes:
- papyrd-storage:/app/storage
depends_on:
postgres:
condition: service_healthy
papyrd-ingest: # OPTIONAL only use if you want to use the ingest directory
image: ghcr.io/rileymathews/papyrd-server:latest
restart: unless-stopped
command: ["/app/ingest"]
environment:
DATABASE_URL: postgres://papyrd:change-me@postgres:5432/papyrd
PAPYRD_SESSION_SECRET: change-this-to-a-long-random-string
PAPYRD_STORAGE_ROOT: /app/storage # MAKE SURE THIS MATCHES THE STORAGE ROOT OF YOUR APP SERVICE
PAPYRD_INGEST_ROOT: /app/ingest
volumes:
- papyrd-storage:/app/storage
- papyrd-ingest:/app/ingest
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_DB: papyrd
POSTGRES_USER: papyrd
POSTGRES_PASSWORD: change-me
volumes:
- papyrd-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U papyrd -d papyrd"]
interval: 5s
timeout: 5s
retries: 5
volumes:
papyrd-storage:
papyrd-ingest:
papyrd-postgres:The ingest container here is completely optional. If running you can drop epub files into the ingest directory via whichever method you have available to you (cp, rsync, scp etc...) and the ingest container will process the files and add the required metadata to the database before copying the file over to the primary storage directory.
The OPDS entrypoint for your server will be at the /opds path. So for example if your server is live at
https://papyrd.mydomain.com then you should use https://papyrd.mydomain.com/opds in your client configurations.
To use the kosync server for progress syncing you should just configure kosync with the root domain of your server. i.e. https://papyrd.mydomain.com
By default the server disables signups after the first user has signed up. You can override this behavior by setting the environment variable
PAPYRD_DISABLE_SIGNUP_AFTER_FIRST_USER to false. This is done as a preventative measure for publically exposed instances.
When signups are disabled, users with the Invite users permission can create one-time invite links from the web app.
Invite links unlock the signup page when passed as /signup?invite=<key>, expire after PAPYRD_INVITE_EXPIRATION_SECONDS,
and can be manually expired from the invite list. The invite expiration defaults to 86400 seconds.
Also by default all users after the first have no 'edit' permissions. Users can read and download all books by default but they cannot delete or upload books and they cannot edit other users permissions. Permissions can be assigned via the users tab in the web app.


