PageKeeper is a self-hosted reading companion that aligns your books across multiple self-hosted platforms, tracks what you read, and also acts as a reading journal for your notes. PageKeeper is a reading tracker: it knows which books you're reading, how far along you are in those books, when you started and finished, and keeps a journal of your progress. On top of that, PageKeeper can sync your position between audiobook and ebook platforms by building an alignment map between the audio and the text. Once that map is built, jumping between formats is seamless.
Right now, PageKeeper is managed by one person. Contributions are welcome - you can read more about how to contribute here.
This project started as a fork of abs-kosync-bridge, a neat project that syncs Audiobookshelf positions across ebooks. Major kudos to cporcellijr for the original idea and implementation.
The goal of PageKeeper is to be a full fledged reading tracking and journaling system that includes a BookFusion integration. While PageKeeper has greatly diverged from the original project, it would not have existed without abs-kosync-bridge. If you're only looking to sync your audio and ebooks across platforms, and don't care about tracking your reading, you might find abs-kosync-bridge to be a better fit for you!
| Platform | What it does |
|---|---|
| Audiobookshelf | Main audiobook server |
| KOReader (via KoSync) | E-ink reader progress (Boox, Kobo, jailbroken Kindle, etc.) |
| Storyteller | Audiobook companion app with synced EPUB3 support |
| Grimmory | Ebook library and shelf manager |
| Hardcover | Book tracking service (write-only) |
| BookFusion | eBook reader, includes excellent EPUB3 support (limited integration) |
You can use as few or as many of the above services as you want. None are required to use the app. If there's another platform you'd liek to see integrated, please open an issue or PR.
PageKeeper runs three sync layers simultaneously, from fastest to slowest:
-
Instant sync — Listens to Audiobookshelf's Socket.IO stream and KOReader's KoSync updates in real time. When you pause an audiobook or push an update from KoReader via KoSync, PageKeeper picks up the change within seconds.
-
Per-client polling — Lightweight checks against individual services (Storyteller, Grimmory) at their own intervals. Only triggers a sync when the position has actually changed.
-
Scheduled full sync — A background sweep every few minutes that catches anything the other layers missed.
When a position change is detected, PageKeeper converts it to every other format (timestamp to percentage, percentage to EPUB position, etc.) and pushes updates to all connected clients. A write-tracker prevents feedback loops — if PageKeeper just pushed a position to a client, it ignores the echo that comes back.
- Create a directory for PageKeeper and download the example compose file:
mkdir pagekeeper && cd pagekeeper
mkdir data
curl -O https://github.com/ghraw/serabi/pagekeeper/main/docker-compose.example.yml
cp docker-compose.example.yml docker-compose.yml-
Edit
docker-compose.ymlto set your timezone and configure any volumes you need (see comments in the file). -
Start PageKeeper:
docker compose up -d- Open the dashboard at
http://localhost:4477and configure your integrations in Settings.
PageKeeper encrypts API keys, tokens, and passwords (e.g. HARDCOVER_TOKEN,
BOOKFUSION_API_KEY, KOSYNC_KEY) before storing them in the SQLite database.
Encryption uses Fernet with a key derived from a master secret that is never
written to the database.
The master secret is discovered in this order:
PAGEKEEPER_SETTINGS_ENCRYPTION_KEY— the recommended, dedicated secret.- The persistent Flask secret (
FLASK_SECRET_KEY, or the auto-generated/data/.flask_secret_keyfile) as a fallback.
To set a dedicated key, generate a strong value and pass it as an environment variable:
python -c "import secrets; print(secrets.token_urlsafe(32))"environment:
- PAGEKEEPER_SETTINGS_ENCRYPTION_KEY=your-generated-secretExisting plaintext secrets are migrated to encrypted form automatically on the
next startup — no manual steps are needed, and the migration is idempotent.
Before that one-way migration encrypts the first secret, PageKeeper writes a
timestamped backup of the database next to it
(database.db.pre-settings-encryption-YYYYMMDD-HHMMSS-*). Backups are created
with owner-only (0600) permissions, and older matching backups are tightened
to 0600 on the next settings migration check. If the backup cannot be
written the migration is skipped and your plaintext secrets are left untouched,
so an upgrade never encrypts without leaving a recovery point. The backup is
only created when there is at least one plaintext secret to migrate.
Database upgrades fail closed when migration history or required schema is
missing. Pre-Alembic databases are upgraded automatically only when all four
original baseline tables (books, hardcover_details, states, and jobs)
and their baseline columns are present. Partial or manually altered legacy
schemas must be restored from backup or repaired before PageKeeper will start;
they are never stamped as current automatically.
Setting a dedicated PAGEKEEPER_SETTINGS_ENCRYPTION_KEY is strongly
recommended: it keeps your encryption key independent of the Flask session
secret, so rotating or regenerating the session secret never strands your stored
secrets. The active key source is logged at startup (the source class only,
never the key value) to help diagnose wrong-key errors.
Key loss still means re-entering secrets. If you lose the master secret,
encrypted secret values cannot be decrypted and must be re-entered in
Settings. The pre-encryption backup gives you an upgrade recovery path (the
pre-migration plaintext), but it is not a substitute for backing the key up
alongside your data/ directory. PageKeeper preserves undecryptable secret rows
rather than overwriting them, and logs which secrets could not be decrypted (by
name only) so the condition is visible.
Key rotation: to rotate the master secret, move the old value to
PAGEKEEPER_SETTINGS_ENCRYPTION_KEY_PREVIOUS and set the new value in
PAGEKEEPER_SETTINGS_ENCRYPTION_KEY. PageKeeper keeps decrypting with the
previous key while re-encrypting writes under the new key. Remove the previous
key once all secrets have been re-saved.
docker compose pull
docker compose up -dBy default, docker-compose.example.yml uses the latest tag. To pin to a specific release:
image: ghcr.io/serabi/pagekeeper:0.1.3Available tags are listed on the packages page.
The core app — sync, dashboard, reading tracker, database — runs on ARM64 (including Raspberry Pi 4/5) with no changes. Expect ~80–150 MB RAM for normal use.
The one caveat is audio↔text alignment via local transcription. PageKeeper bundles faster-whisper for speech-to-text, which depends on ctranslate2 — a library that can be difficult to build on ARM. If you run into build failures on ARM, you have three alternatives:
- Storyteller native alignment — If your audiobooks are in Storyteller, PageKeeper can read its pre-computed word-level timelines directly. No local transcription needed.
- Whisper.cpp server — Run a whisper.cpp HTTP server on a more capable machine on your network, then point PageKeeper at it in Settings → Transcription Provider.
- Deepgram — Use Deepgram as a cloud transcription provider (configured in Settings). This offloads the work to an API and avoids the ARM build issue entirely.
If you do manage to get faster-whisper running on ARM, note that local Whisper transcription is memory-intensive: the base model needs ~1 GB RAM, while large-v3 can require 4–8 GB.
This app is built with Python 3.13 and Flask, using SQLAlchemy with SQLite for persistence and Alembic for database migrations. It runs in a Docker container based on python:3.13-slim, with ffmpeg for audio processing and faster-whisper installed in-container for speech-to-text transcription. The frontend uses vanilla JavaScript, HTML, and CSS. Dependency injection is handled by dependency-injector, and real-time communication with Audiobookshelf uses python-socketio.
This app does rely heavily on AI coding tools. The tools are directed by a human and I do read the code generated, but I do want to be up front that I am not writing most of the code directly. I run Snyk security to help catch security issues, and CodeRabbit.io's free plan on every PR to help catch bugs in the code. I have made every effort to make this app secure and stable, but there may be issues. Please report any issues that you find!

