A go port of booklore/grimmory
  • Go 71.8%
  • JavaScript 22.7%
  • HTML 3.6%
  • CSS 1.7%
  • Shell 0.1%
Find a file
Jeremy Bush f027793508
Some checks are pending
/ Lint (push) Has started running
/ Test (push) Has started running
/ E2E Browser (push) Has started running
/ JS Unit Tests (push) Successful in 1m17s
/ E2E API (push) Successful in 2m46s
/ Integration (push) Successful in 3m42s
/ Build and publish (amd64) (push) Successful in 3m48s
Merge pull request 'fix(middleware): data race + cross-user contamination in WrapHealthCheck (bookshelf-n51o)' (#897) from bd-bookshelf-n51o into main
2026-07-03 20:24:57 +00:00
.beads bd init: initialize beads issue tracking 2026-05-20 11:11:12 -05:00
.claude docs(review-standard): forbid grandfathering new golangci debt (repath-only) 2026-07-03 14:30:17 -05:00
.forgejo/workflows fix(ci): use grep instead of file cmd for static-binary guard (file not in pergamum-ci) 2026-06-30 07:08:10 -05:00
cmd/pergamum fix(governor): put governor under coverage gate + fail-closed unconfigured rate path (bookshelf-e47e.1) 2026-06-28 20:34:27 -05:00
data bd-bookshelf-qqz.1: scaffold repo structure 2026-05-20 11:46:44 -05:00
docker/mysql-ci fix(ci-mysql): add forgejo.lan kaniko auth + align max_connections to runtime value 2026-06-16 23:04:45 -05:00
docs ci: extract SCREENSHOT_JOURNEY from [shot:<slug>] PR title marker (bookshelf-qy9b) 2026-06-28 14:24:13 -05:00
e2e fix(ux): preserve book selection after bulk enrich/llm-vision/custom-fetch ops 2026-07-03 12:51:57 -05:00
internal fix(middleware): restructure WrapHealthCheck test It blocks per conventions (bookshelf-n51o) 2026-07-03 15:19:02 -05:00
loadtest docs: finish bookshelf→pergamum rename sweep (bookshelf-sw5a) 2026-06-11 21:49:38 -05:00
scripts chore: add screenshot script for comic prefill visual gate (bookshelf-7mzg) 2026-07-02 11:58:03 -05:00
static fix(ux): preserve book selection after bulk enrich/llm-vision/custom-fetch ops 2026-07-03 12:51:57 -05:00
templates fix(wfengine): fill completed-at cell on live status transition (bookshelf-3u1y) 2026-07-01 14:56:56 -05:00
.dockerignore bd-bookshelf-qqz.1: scaffold repo structure 2026-05-20 11:46:44 -05:00
.editorconfig bd-bookshelf-qqz.1: scaffold repo structure 2026-05-20 11:46:44 -05:00
.gitignore chore: ignore screenshot harness output PNG (bookshelf-u7vb.4) 2026-06-25 11:42:07 -05:00
.gitmodules feat(docs): stand up Hugo + Hextra docs site infrastructure (bookshelf-xpd.1) 2026-05-27 22:59:37 -05:00
.golangci.yml fix(lint): tighten gocyclo to CC>15, document nestif blind spot 2026-07-03 13:56:43 -05:00
AGENTS.md bd init: initialize beads issue tracking 2026-05-20 11:11:12 -05:00
CLAUDE.md feat(reviews): ui-reviewer agent + visual gate for template/CSS PRs (bookshelf-g3y8) 2026-06-18 19:32:52 -05:00
dev.env.example feat(config): rename default database name bookshelf → pergamum (bookshelf-50z.3) 2026-06-02 20:13:37 -05:00
docker-compose.worker.yml docs(worker): update docker-compose.worker.yml for go-workflows engine (bookshelf-3j83) 2026-06-29 06:25:07 -05:00
docker-compose.yml feat(dev): parameterize host ports to prevent worktree stack collisions 2026-06-24 12:33:06 -05:00
Dockerfile fix(docker): build static (CGO_ENABLED=0 + -tags nodynamic) to fix prod musl-loader crash (bookshelf-jxqf) 2026-06-30 07:08:10 -05:00
Dockerfile.ci rename: CI image bookshelf-ci → pergamum-ci + stale bookshelf strings in docs (bookshelf-50z.6) 2026-06-11 12:45:12 -05:00
Dockerfile.mysql-ci feat(ci): tune CI test-MySQL for throwaway speed (bookshelf-uyem) 2026-06-16 23:04:45 -05:00
go.mod feat(cover): decode JPEG XL comic covers (bookshelf-d4yf.1) 2026-06-29 20:53:05 -05:00
go.sum feat(cover): decode JPEG XL comic covers (bookshelf-d4yf.1) 2026-06-29 20:53:05 -05:00
Makefile fix(ci): use grep instead of file cmd for static-binary guard (file not in pergamum-ci) 2026-06-30 07:08:10 -05:00
package-lock.json ci(js): add Vitest coverage measurement (no gate yet) (bookshelf-y59n.5) 2026-06-17 08:35:58 -05:00
package.json ci(js): add Vitest coverage measurement (no gate yet) (bookshelf-y59n.5) 2026-06-17 08:35:58 -05:00
README.md docs: finish bookshelf→pergamum rename sweep (bookshelf-sw5a) 2026-06-11 21:49:38 -05:00
sqlc.yaml feat(sqlc): wire up sqlc code generation with smoke query (bookshelf-qqz.5) 2026-05-20 15:16:46 -05:00
vitest.config.js feat(ci): enforce JS coverage gate + migrate test loads off eval (bookshelf-1257.1) 2026-06-22 15:50:26 -05:00

Pergamum

A self-hosted digital library server for eBooks, comics, PDFs, and audiobooks. Grimmory-compatible schema rewrite in Go.

Local development

Copy dev.env.example to ~/.config/pergamum/dev.env, fill in your values (API keys, admin credentials, books directory, host uid:gid), then run make up. The file lives outside the repo so it is never committed. Every worktree's make up / make dev / make down reads it automatically when present.

Commands

pergamum [FLAGS] [SUBCOMMAND [FLAGS] ...]

With no subcommand, the HTTP server starts (equivalent to pergamum serve).

serve (default)

Start the HTTP server.

pergamum [FLAGS]
pergamum serve [FLAGS]

Both forms start the HTTP server. The bare form is provided for backward compatibility with existing scripts and docker-compose files.

scan

Synchronously scan a configured library and populate the database with books. Useful for initial seeding and development without needing background workers.

pergamum scan <library_id>

A library must be created via the HTTP API before scanning. Example sequence:

docker compose up -d   # start server

# create the library
curl -X POST -H 'Content-Type: application/json' \
  -d '{"name":"My Books","organization_mode":"BOOK_PER_FILE","allowed_formats":"epub,pdf"}' \
  http://localhost:8080/libraries

# add a path to the library (use the library id returned above)
curl -X POST -H 'Content-Type: application/json' \
  -d '{"path":"/srv/books"}' \
  http://localhost:8080/libraries/1/paths

# scan the library
docker compose run --rm pergamum scan 1

All output is structured JSON (via log/slog). Progress logs are emitted every 100 files or 10 seconds, whichever comes first.

Example:

PERGAMUM_DSN="user:pass@tcp(localhost:3306)/bookshelf?parseTime=true" pergamum scan 1

worker

Run the background task pool as a standalone process (no HTTP server).

pergamum worker [FLAGS]

See Deployment — worker modes below for when to use this subcommand.

Configuration

Configuration is loaded from flags, PERGAMUM_* environment variables, and an optional config file (key=value lines). Precedence: flags > env > config file > built-in defaults.

Flags (shared by all subcommands):

  • --http-addr — HTTP server listen address (default :8080)
  • --metrics-addr — Prometheus metrics listen address (default: same as --http-addr; pergamum worker uses this for /metrics, /healthz, and /readyz)
  • --dsn — MySQL DSN
  • --data-dir — root directory for derived data (covers, thumbnails)
  • --log-leveldebug, info, warn, error (default info)
  • --log-formatjson or text (default json)
  • --config — path to optional config file
  • --slow-query-log — log queries slower than this threshold (default 100ms, 0 disables)
  • --worker-count — number of concurrent worker goroutines (default 4)
  • --worker-modeembedded, external, or disabled (default embedded; see below)

Deployment — worker modes

Pergamum supports three worker pool topologies selected by --worker-mode / PERGAMUM_WORKER_MODE.

embedded (default)

The HTTP server starts and owns the worker pool in the same process.

pergamum           # or: PERGAMUM_WORKER_MODE=embedded pergamum

Suitable for single-process deployments and local development (docker compose up).

external

The HTTP server does not start a pool. A separate pergamum worker process handles task processing.

# Terminal 1 — HTTP server only
PERGAMUM_WORKER_MODE=external pergamum

# Terminal 2 — standalone worker pool
PERGAMUM_METRICS_ADDR=:9091 pergamum worker

Use the compose override to run this topology with Docker Compose:

docker compose -f docker-compose.yml -f docker-compose.worker.yml up

Note: After pulling changes that add database migrations, rebuild the Docker image to ensure the app's embedded migrations are up to date (docker compose ... up --build). Docker Compose reuses cached images by default; without a rebuild, the binary's embedded migrations may lag behind the database, resulting in "no migration found for version N" errors.

Scale worker replicas horizontally (each replica is a separate pool; they safely contend on the tasks table via the CAS-claim protocol):

docker compose -f docker-compose.yml -f docker-compose.worker.yml up --scale worker=N

disabled

Neither the HTTP server nor a separate pergamum worker process runs a pool. Use for read-only replicas or environments where task processing is handled elsewhere.

PERGAMUM_WORKER_MODE=disabled pergamum

Single-cron-owner rule

Cron always runs in the HTTP server process (embedded or external mode) — never in pergamum worker.

The cron scheduler is a singleton owned by the app process. In external mode, the app enqueues tasks on schedule and the worker process claims them. In disabled mode, no tasks are enqueued. Multiple pergamum worker replicas never race on cron because they do not run it.

Worker health endpoints

The standalone pergamum worker process exposes three endpoints on PERGAMUM_METRICS_ADDR:

Endpoint Purpose Success
/metrics Prometheus scrape target 200
/healthz Liveness — process is alive and serving 200
/readyz Readiness — DB is reachable; worker can claim tasks 200

/readyz pings the database with a 3-second timeout and returns 503 Service Unavailable when the ping fails, allowing orchestrators to hold traffic until the worker is ready.

Deployment

See docs/releases.md for Docker image streams, self-hoster setup, release procedures, and versioning policy.

Help

pergamum --help
pergamum serve --help
pergamum scan --help
pergamum worker --help