Self-hosted book library manager with deep KOReader integration and rich reading statistics. Only meant for my own personal use and it is perpetually in development.
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
||
Shelfloom can also be installed to your phone's home screen (Add to Home Screen) and opened full screen like an app. Press ⌘K / Ctrl+K or / anywhere to search.
Images are published automatically to GHCR on every push to main and on version tags (v*).
Copy the example compose file and edit it for your machine:
cp docker-compose.example.yml docker-compose.ymlThe tracked example file is docker-compose.example.yml. Your local
docker-compose.yml is ignored so machine-specific paths do not keep showing up
as unstaged changes.
Example:
services:
shelfloom:
image: ghcr.io/audemed44/shelfloom:latest
ports:
- "8000:8000"
volumes:
- ./.data:/data
- ./.data/books:/books
restart: unless-stoppedThen:
mkdir -p .data/books .data/covers
docker compose up -dTo pin to a specific release, use a version tag instead of latest:
image: ghcr.io/audemed44/shelfloom:v1.0.0git clone https://github.com/audemed44/shelfloom.git
cd shelfloom
mkdir -p .data/books .data/covers
# Start the container (builds the image locally)
docker compose up -d --buildOpen http://localhost:8000 — the setup wizard will guide you through creating your first shelf.
The default docker-compose.example.yml uses a .data/ directory in the repo root:
| Host path | Container path | Purpose |
|---|---|---|
./.data |
/data |
Database and cover images |
./.data/books |
/books |
Book files (your shelf) |
Set HOMEPAGE_URL to your Foyer
address (e.g. https://home.example.com) to get a link back to it at the top
of the sidebar, and in the More menu on phones.
With a Z-Library account set (SHELFLOOM_ZLIBRARY_EMAIL and
SHELFLOOM_ZLIBRARY_PASSWORD), the Library page gets a Z-Library button:
search by title, author or ISBN, and Add to library downloads the EPUB or
PDF into the default shelf. Details the file lacks (author, ISBN, year,
description, cover) are filled in from Z-Library, then the book opens in the
editor to set its series, genres and tags. Each add uses one of the account's
daily downloads; a book whose file is already in the library is not downloaded
again.
Z-Library has no official API: Shelfloom uses the one its apps use, which can
change. Its domain moves too: Settings → Z-Library lists its published
domains together with the current list Z-Library's own API gives out, checks
which ones answer from your server, and lets you switch domain or add a new
one. Take new domains only from Z-Library's own
channels: lookalike domains exist to steal account passwords.
SHELFLOOM_ZLIBRARY_DOMAIN is only the starting choice (HTTPS_PROXY is
honoured if the server needs a proxy).
With an OpenRouter API key set (OPENROUTER_API_KEY),
For You (in the sidebar, or More on phones) suggests what to read:
- Read next picks from the unread books in your library: the next book of each series and unread standalones. The top picks also show on Home.
- Discover suggests books and web serials you don't have, optionally searching the web (Royal Road, Reddit, Goodreads, Scribble Hub, Novel Updates) to find newer and lesser-known ones. Books link to a Z-Library search, Royal Road serials to Royal Road.
The model is told what you read, grouped by series and web serial: ratings, reading time, when you last read it, DNFs, genres and tags, the serials you follow and your feedback on earlier picks (not for me, read it, doesn't exist, saved). Reviews and highlights are only sent when allowed in Settings → Recommendations, which is also where the model is picked (any OpenRouter model; the default is Google's Gemini 3.8 Flash) and weekly refreshes are turned on. A refresh sends about 10k tokens per list and costs a cent or a few; each run's tokens, searches and cost are shown under the list.
Shelfloom runs a sync server for KOReader's built-in Progress sync plugin, and has a web reader for EPUBs (the Read button on a book). Both share one reading position per book, so you can read a chapter in the browser and pick up on your e-reader, or the other way round. No KOReader plugin is needed.
- In KOReader, open a book and go to Tools → Progress sync → Custom sync server. Enter
http://<your-host>:8000/api/kosync(Settings → KOReader Sync shows the exact address). - Register / Login, with a new account or one created in Settings → KOReader Sync.
- Turn on Automatically keep documents in sync. Under Sync behavior, "Sync to a newer state: Silently" and "Sync to an older state: Prompt" work well. Leave Document matching method on Binary.
Books are matched by file, so this works however the files reach the device (Syncthing, USB, OPDS). Positions follow a book when its file changes, including when a web serial volume is rebuilt with chapters added. The web reader converts positions to KOReader's own format; frontend/scripts/koreader-xpointer-check checks that conversion against KOReader's engine.
Place .sdr folders alongside their book files in your books directory. Shelfloom will automatically import highlights, bookmarks, and reading sessions during shelf scans.
If you have a KOReader statistics.sqlite3 file, mount it into the container and provide the path when triggering a scan via the API.
Shelfloom writes one JSON object per line on stderr, for
Logbook to collect: every request
(failures as errors, changes at info, plain reads at debug) and its
background jobs with how long they took: library scans (logged when they
change something or fail), serial checks and recommendation runs. A
message repeated more than 20 times a minute is held back and counted, at
most 600 lines a minute get out, and long values are cut at 4 KB, so a
failing Shelfloom can't flood the disk. Docker's own copy is capped in
compose (logging:, 3 × 10 MB).
Shelfloom is a Go server (cmd/shelfloom, internal/) that serves the API
and the built React frontend (frontend/), with SQLite for storage.
Requirements: Go (see go.mod), Node 20+, and poppler-utils for PDFs.
npm install # root dev tools and the frontend
npm run dev # Go server on :8000 (data in .data/) and Vite on :5173npm test # go test + vitest
npm run lint # gofmt + go vet, eslint| Variable | Default | |
|---|---|---|
SHELFLOOM_DB_PATH |
/data/shelfloom.db |
Database file |
SHELFLOOM_COVERS_DIR |
/data/covers |
Cover images |
SHELFLOOM_LISTEN |
:8000 |
Listen address |
SHELFLOOM_SCAN_INTERVAL |
300 |
Seconds between library scans |
SHELFLOOM_SERIAL_CHECK_INTERVAL |
86400 |
Seconds between web serial update checks |
LOG_LEVEL |
info |
debug, info, warn or error (SHELFLOOM_LOG_LEVEL still works) |
LOG_FORMAT |
json |
text for reading in a terminal |
HOMEPAGE_URL |
Foyer address for the sidebar link | |
SHELFLOOM_ZLIBRARY_EMAIL |
Z-Library account email | |
SHELFLOOM_ZLIBRARY_PASSWORD |
Z-Library account password | |
SHELFLOOM_ZLIBRARY_DOMAIN |
z-library.hn |
Z-Library domain until one is picked in Settings |
OPENROUTER_API_KEY |
OpenRouter key for recommendations (SHELFLOOM_OPENROUTER_API_KEY wins if both are set) |
|
SHELFLOOM_OPENROUTER_MODEL |
google/gemini-3.8-flash |
Model until one is picked in Settings |
SHELFLOOM_OPENROUTER_BASE_URL |
https://openrouter.ai/api/v1 |
OpenRouter API address (a proxy or test server) |
TZ |
Time zone stats are grouped in |
Databases from the earlier Python-based releases open as they are, as long as that release had upgraded them to its last migration (it does so on start).




















