* fix(kobo): forward x-kobo-userkey and decode the composite sync token in the catch-all proxy The catch-all proxy in kobo-device.controller.ts forwards device requests to storeapi.kobo.com through KoboProxyService, which built its outgoing headers from a static allow-list (FORWARD_HEADERS). The list covered accept, accept-language, authorization, content-type, user-agent, and a curated set of x-kobo-* headers, but two of the headers that matter for store-API authentication and cursor tracking were missing: x-kobo-userkey, the device's per-account secret that authorizes most Kobo store endpoints, and x-kobo-synctoken, the upstream cursor the device carries on every store call. Without x-kobo-userkey, every store interaction proxied through the catch-all shipped without credentials, so Kobo either rejected the call or treated the device as unauthenticated. The "Connecting to OverDrive" dialog that issue #738 describes is the borrow endpoint, /v1/library/borrow, hanging because Kobo cannot reconcile a borrow into a user's library without the user key. The same gap also left library books a user borrowed on libbyapp.com (which is just OverDrive pushed to kobo.com) unreachable from the device, because every proxy-bound sync the device makes lands at Kobo without the credentials it needs to enumerate the user's entitlements. The Kobo Plus / Kobo Store entitlement fix in 2.6.0 (closes #789) only patched the one explicit library/sync call. That call already decodes the BookOrbit composite sync token via kobo-sync-token.ts and overrides the outgoing header with the Kobo cursor Kobo understands, so the store page works. Every other store call still went through the catch-all proxy with the device's PX.<base64> composite on it, which Kobo treats as a fresh cursor and answers for. The borrow endpoint in particular was the most visible casualty, but anything else proxied through @All('*') was affected the same way. Switch the proxy to a deny-list (DROP_HEADERS) for the headers RFC 7230 names hop-by-hop or otherwise scoped to a single connection (host, connection, content-length, content-encoding, transfer-encoding, keep-alive, proxy-authenticate, proxy-authorization, te, trailers, upgrade), and forward everything else the device sends. x-kobo-userkey now reaches Kobo on every store call, so the borrow endpoint can authorize the user's library and the libby-arrived entitlements surface on the next /v1/library/sync. Add a forward() path that decodes the device's composite sync token before relaying, and forwards the Kobo cursor Kobo can use instead. A raw Kobo token (no PX. prefix) is forwarded verbatim, the way devices that synced with Kobo directly before BookOrbit was put in the path already do. A composite token whose cursor is undefined has no Kobo-side value, so omit the header entirely on the way out rather than leak our composite to Kobo. The extraHeaders and omitHeaders escape hatches stay in place, so the explicit library/sync call can still pin the upstream cursor (and the no-cursor path still drops the header) without going through the new decode-on-forward path. The librarySync controller in kobo-sync.controller.ts already uses those exacts knobs for the store page, so this change does not touch it; this commit is the catch-all side of the same fix. Closes #738 * fix(kobo): honor RFC 7230 Trailer and Connection fields, and emit sanitized proxy logs Three small corrections surfaced by code review on the catch-all proxy that delivers Kobo store entitlements. The deny-list shipped with the wrong name for one of the RFC 7230 hop-by-hop fields. `trailers` (plural) does not match the standard `Trailer` header that arrives in mixed case, so a Trailer field set by an upstream could leak through on the way to the device. Replace it with `trailer`. RFC 7230 says the fields listed in `Connection` are also hop-specific, and a proxy must strip them before forwarding. The two header-copy paths only checked DROP_HEADERS, so a `Connection: x-kobo-tracking` header from the device or from upstream could let a nominated field slip through. Parse the Connection header in both directions, normalize case-insensitively, and add its fields to the ignore set alongside DROP_HEADERS. The new readConnectionFieldNames helper does the splitting once and is reused on both sides of the relay. Add a structured start/end/fail log around the relay, replacing the previous single-line warn that interpolated a request-derived targetUrl and a raw err.message. The repo's logging conventions in AGENTS.md name an external HTTP call as a non-trivial operation that warrants start/end/fail logs, and require sanitizeLogValue() for every dynamic value that lands inside a quoted log field. The fail line now carries durationMs, errorClass, sanitized error, and sanitized targetUrl. The 502 response with the "Upstream Kobo API unavailable" body is preserved. Tests cover the new paths: Trailer is stripped, Connection-nominated fields are stripped on the request side, and Connection-nominated fields are stripped on the response side. All 10,486 server tests pass; lint and typecheck clean. CodeRabbit also asked to instantiate KoboProxyService through Test.createTestingModule. AGENTS.md says the same thing, but every sibling test in the kobo module uses direct construction with no testing module, so changing only this file would invert the local pattern. The convention is already inconsistent across the module and is not enforced by any lint rule; that is a separate cleanup better suited to its own PR. * test(kobo): drop unused makeHeaders helper in proxy service tests makeHeaders was a placeholder identity function that returned its argument unchanged. Inline the headers object literals at the 9 call sites. * fix(kobo): scrub BookOrbit credentials and the composite token in every relay Move the sync-token substitution into request() so forwardTagDelete, which relays with no options, stops handing Kobo a PX. token it cannot read. Apply it before omitHeaders and extraHeaders so store sync keeps both escape hatches. Strip BookOrbit's session cookies and the reverse proxy forwarding headers: these routes sit on the BookOrbit origin, so a browser carrying a session reached them with our auth cookies attached and relayed them to Kobo. Restore the SSRF regression case that covers the allowed hostname in userinfo. * test(maintenance): wait for the cover sweep instead of a fixed turn budget The detached sweep does real filesystem I/O, so 50 setImmediate turns settled it locally but not on a loaded runner. The drain loop then gave up silently and the next call failed as "The cover sweep is not complete", pointing at the wrong place. Poll the store to a deadline and fail loudly when it truly hangs. --------- Co-authored-by: Daniel Chong <daniel@chong.nz> 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: Community translations are managed on Crowdin. See the localization guide for current language support and contributor instructions.
- 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 > KOReader, create credentials if prompted, 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 a one-time import from Audiobookshelf, see the Audiobookshelf migration guide. For a stopped-snapshot import from Calibre-Web Automated, see the Calibre-Web Automated migration guide. 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.
Repository Activity
Translations
Help translate BookOrbit into your language on Crowdin.
When adding user-facing text in code, add the Vue I18n key only to client/src/locales/en.json. Do not edit non-English catalogs in a feature pull request; untranslated keys fall back to English until Crowdin provides a translation. See docs/LOCALIZATION.md for the complete workflow.
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.

