* feat(koreader): add dashboard row configuration model * feat(koreader): reuse catalog for dashboard source selection * feat(koreader): configure dashboard rows on 1.4.0 * chore(koreader): preserve dashboard source line endings * fix(koreader): keep BookOrbit tools menu loadable * fix(koreader): keep dashboard requests out of rendering * revert(koreader): restore stock 1.4.0 dashboard renderer * revert(koreader): use stock single dashboard row * fix(koreader): apply selected dashboard row source * feat(koreader): add second dashboard row setting * feat(koreader): render second dashboard row * fix(koreader): fetch configured dashboard rows directly * fix(koreader): honor configured dashboard row sources * feat(koreader): support four configurable dashboard sections * fix(koreader): use real catalog dashboard sources * fix(koreader): select dashboard shelves from catalog * fix(koreader): reuse catalog for dashboard source selection * fix(koreader): close dashboard menu before catalog selection * test(koreader): split dashboard configuration coverage * chore(koreader): bump version to 1.5.0 * fix(koreader): address dashboard review feedback * fix(koreader): address dashboard section review feedback * test(koreader): cover dashboard behavior at runtime * test(koreader): use Nest setup for dashboard DTO * chore(ci): trigger pipeline * fix(koreader): migrate untagged dashboard section settings Plugin 1.4.x stored the configurable dashboard rows as a bare ordered list with no schema marker, so gating the migration on schemaVersion == 1 never matched anything a device had actually written. Every upgrading user who had ever picked a row was read positionally instead: the Stats strip disappeared and Discover ended up in two slots, with a stored SmartScope dropped entirely. Treat an untagged stored list as the legacy shape and migrate it into the slots those rows used to occupy. Cover it with the settings shapes 1.4.x really wrote, rather than a marker nothing ever produced. Also drop store() and primary(), which no caller uses now that slots are written through storeAt(), along with the dead grow loop inside storeAt() that normalize() already makes unreachable. * fix(koreader): keep dashboard shelves in aspect, inside the page, and deduped The four-slot renderer handed each shelf a height derived from the leftover vertical budget while its cover cards kept the width computed from the natural row, so any page with more than one shelf drew covers out of aspect. It also had no answer for a page too small for the configured slots: shelves clamped to a 48px floor and the layout simply overflowed the available height. Size shelves from their cards instead. A shelf that does not fit gets more, narrower cards per row, so its height is always the height those cards need. When even the narrowest row does not fit, drop slots the way the pre-four-slot renderer did: the stats strip first, then shelves from the bottom up. Fetch each distinct shelf source once per dashboard load. Two slots showing Discover previously rendered the same list twice, and two slots on the same catalog filter each paid for their own request; a source that fails is now also not retried by the next slot that wants it. Drop what the four-slot rewrite left behind: the unused capabilities require, the tall-layout helper and its constants, and the section accessors and request helper that no longer have callers. Name the per-shelf page size and the minimum cover width instead of repeating the literals. Add setDashboardSections() so a whole configuration can be applied in one refresh. Add a layout spec that drives updateDashboardItems and asserts covers stay in aspect, the drop order holds, and nothing is laid out past the available height. * fix(koreader): close the dashboard slot menu through its own API Reset to Default wrote section_stale as a boolean where the renderer reads a per-slot table, so the reset never actually marked anything pending and stale books stayed on screen under the new headers until the refresh landed. It also dereferenced current_context without a guard and wrote the setting behind the catalog's back. Route it through the new setDashboardSections() so the slots and the cache signature move together in one refresh. Close the invoking touch menu with closeMenu() rather than handing the widget to UIManager:close(), which skips the close_callback that owns its container. Drop the section parameter from catalogDashboard() and the unused catalogDashboardSection() helper: the plugin builds its shelves from the ordinary catalog endpoints now and sends neither. The server keeps both routes for devices still on 1.4.x, so its DTO test stays; rename it to match the file it covers and drop the empty Nest module compile. * revert(koreader): keep the plugin at 1.4.0 1.4.0 has not shipped yet: 1.3.0 is the last released plugin version, so the dashboard slot work can land inside 1.4.0 rather than claiming a version of its own. Devices in the field are on 1.3.0 and see a single 1.3.0 -> 1.4.0 step either way. * fix(koreader): stop rotating upright read-status badges buildReadStatusBadge turned every status icon a quarter turn, but only two of the seven are corner folds. The dogear assets are drawn with their right angle at the top left and the badge sits in the cover's top-right corner, so those do need the turn. Bookmark, check and reload are upright glyphs and were being drawn on their side: a want-to-read cover showed its bookmark ribbon lying sideways like a chevron. Rotate only the dogear icons. --------- Co-authored-by: neon <18559658+neonsolstice@users.noreply.github.com>
What is BookOrbit?
BookOrbit organizes your books and reads them back to you anywhere: the web reader, a Kobo, or KOReader. Progress, highlights, and reading status move between all three, so you can start a chapter in one place and finish it in another.
Around that core sit 14 metadata providers, reading statistics and achievements, OPDS and Send-to-Kindle delivery, multi-user accounts with OIDC/SSO, and automatic sync out to Hardcover, Readwise, and StoryGraph. All of it runs on infrastructure you control.
Live Demo
Try the live instance before you install. No account required.
Note: The demo includes a sample library of public domain books. Some administrative features are limited in the public demo. Self-hosting BookOrbit provides the full experience.
Features
Reading Experience & Sync
- Built-in Web Readers: Ebooks (EPUB, KEPUB, MOBI, AZW3, AZW, FB2), PDFs, comics (CBZ, CBR, CB7), and audiobooks (M4B, MP3, M4A, OPUS, OGG, FLAC), with no extra plugins required.
- Three-Way Sync (Kobo + KOReader + BookOrbit): Progress and annotations flow bidirectionally between Kobo devices, KOReader, and the BookOrbit web reader. Pick up on any surface where you left off on another, including highlights and deletions.
- KOReader Plugin: An on-device catalog browser with search, download, and status and rating management, alongside full progress and annotation sync.
- Annotations & Highlights: Highlights from the web reader, KOReader, and Kobo merge into one searchable hub. Filter by color, style, and source; export as Markdown, CSV, or JSON.
- Hardcover, Readwise & StoryGraph Sync: Push status, progress, reading dates, and ratings to Hardcover on configurable triggers; status and progress to The StoryGraph; and new highlights and notes to Readwise as you create them, from both the web reader and synced devices. Hardcover read history can be pulled back to backfill blank BookOrbit entries.
- Statistics, Goals & Achievements: Daily reading time, heatmaps, streaks, and library health, plus yearly goals, monthly challenges, and 50+ achievements across five categories. Reading DNA profiles your reading style from your actual session history.
Library Management
- Multiple Libraries: Isolate content with per-library folders, custom scan rules, and format priorities.
- 14 Metadata Providers: Google Books, Open Library, Amazon, Goodreads, Kobo, Hardcover, Audible, Audnexus, Libro.fm, and iTunes, plus ComicVine for comics, RanobeDB for light novels, and Aladin and Lubimyczytać for Korean and Polish catalogs. Cover art is sourced separately from iTunes, DuckDuckGo, and AudiobookCovers.
- Smart Scopes & Collections: Organize your collection with curated lists and dynamic, rule-based saved filters.
Platform & Delivery
- Multi-User & SSO: Granular per-user permissions and isolated reading data, with native support for Authentik, Keycloak, and Authelia via OIDC.
- Multilingual Interface: English, German, Spanish, French, Italian, Dutch, Polish, Brazilian Portuguese, and Slovenian, translated by the community on Crowdin.
- Content Delivery: OPDS support for compatible apps, Send-to-Kindle via email, and browser drag-and-drop uploads.
- Automated Ingestion: Configure a Book Dock drop folder for hands-free importing.
Quick Start (Docker)
mkdir bookorbit && cd bookorbit
mkdir -p books data/app data/postgres
curl -fsSLo .env https://raw.githubusercontent.com/bookorbit/bookorbit/main/.env.example
curl -fsSLo docker-compose.yml https://raw.githubusercontent.com/bookorbit/bookorbit/main/docker-compose.yml
Edit .env and set these required values:
APP_URL=http://your-server-ip:3000 # the URL you'll open in your browser
BOOKS_HOST_PATH=./books # folder on your server where your book files live
POSTGRES_PASSWORD= # database password - openssl rand -hex 24
JWT_SECRET= # signs login tokens - openssl rand -hex 32
SETUP_BOOTSTRAP_TOKEN= # one-time setup wizard token - openssl rand -hex 16
On a NAS, or any host where your book folder is owned by a user other than UID 1000, also set PUID and PGID to match that owner. Run id -u and id -g as the owning user to find them. Getting these wrong is the most common cause of permission errors on first scan.
Optionally set LIBRARY_BROWSE_ROOT=/books to start the library folder picker at /books instead of /.
Then start:
docker compose up -d
Open http://your-server-ip:3000 and complete setup using your SETUP_BOOTSTRAP_TOKEN.
For the full installation guide including reverse proxy setup, file permissions on NAS, external databases, and environment variable reference, see bookorbit.app/installation.
KOReader Plugin
The BookOrbit plugin for KOReader adds progress sync, two-way annotation sync, and a native catalog browser: navigate, search, and download books from your library without leaving the device.
- In BookOrbit, go to Settings > Integrations > KOReader and click Download Plugin.
- Unzip
bookorbit.koplugin.zip. - Copy
bookorbit.koplugintokoreader/plugins/on the device. - Restart KOReader and open a book.
- Use Tools > BookOrbit Sync to connect.
The download is pre-configured with your server URL and credentials, so there is no manual entry on the device. For full setup and sync options, see bookorbit.app/koreader-plugin.
Documentation and Contributing
Full documentation is at bookorbit.app, covering libraries, metadata, readers, Kobo sync, OPDS, users and permissions, OIDC setup, and more.
For local development, see docs/DEVELOPMENT.md. To contribute, see docs/CONTRIBUTING.md for the full workflow: branch naming, test expectations, PR checklist, and commit format.
Translations
Help translate BookOrbit into your language on Crowdin.
Star History
Support
- Questions and discussion: GitHub Discussions
- Bug reports: GitHub Issues
- Feature requests: GitHub Issues
- Security vulnerabilities: Follow the private reporting process in the Security Policy.
License
BookOrbit is licensed under the GNU Affero General Public License v3.0.

