Kobo Shelf — Consolidated Knowledge Base #
Single source of truth distilled from the prior scratch docs
(SYNC_HANDOFF.md, docs/kobo.md, docs/komga-kobo-findings.md). Covers the
Kobo device itself, the wipe traps, every self-hosting path, the kobo-shelf store-
bypass work, the Komga feature comparison, and operational/runtime setup.
Status note. This doc reconciles the old scratch notes against the current repo. Items marked [SHIPPED] are already in
master; [PENDING] are still open. Verify against code before relying on a status claim.
1. Hardware & device filesystem #
- Test device: Kobo Libra Colour, model
0390/N428/Kobo_monza, seriala16104b1ef0ce84c94893b424dc226d1(visible in sync requests asSerialNumber=...). - On-device storage root:
/mnt/onboard/. Mounts on Linux as/run/media/<user>/KOBOeReader/. KOReader sees the same paths. - Hidden config tree:
/mnt/onboard/.kobo/(note the leading dot). - Tested on FW
4.42.23296→4.45.23684(May 2026). The device factory-wiped twice during testing — see §4. - Suspend depth: stock Nickel weeks–months standby; KOReader ~1–2 weeks.
2. Authentication — what lives where #
Three pieces interact:
/.kobo/Kobo/Kobo eReader.conf— INI-ish config. Sections:[ApplicationPreferences](device flags),[OneStoreServices](server URLs),[ReadingLife](cached account state)./.kobo/KoboReader.sqlite— main DB. Theusertable holds the registered account./.kobo/affiliate.conf— affiliate sync timestamp; uninteresting.
2.1 user table schema (FW 4.42–4.45) — 29 columns #
Older recipes assumed 27; Libra Colour has 29. Don't trust pre-2024 SQL blindly.
user(
UserID TEXT NOT NULL, UserKey TEXT NOT NULL, UserDisplayName TEXT, UserEmail TEXT,
___DeviceID TEXT, FacebookAuthToken TEXT, HasMadePurchase BIT DEFAULT FALSE,
IsOneStoreAccount BIT DEFAULT FALSE, IsChildAccount BIT DEFAULT FALSE,
RefreshToken TEXT, AuthToken TEXT, AuthType TEXT, Loyalty BLOB,
IsLibraryMigrated BIT NOT NULL DEFAULT true, SyncContinuationToken TEXT,
Subscription INT NOT NULL DEFAULT 0, LibrarySyncType TEXT, LibrarySyncTime TEXT,
SyncTokenAppVersion TEXT, Storefront TEXT, NewUserPromoCurrency TEXT,
NewUserPromoValue REAL NOT NULL DEFAULT -1.0, KoboAccessToken TEXT,
KoboAccessTokenExpiry TEXT, AnnotationsSyncToken TEXT, PrivacyPermissions BLOB,
AnnotationsMigrated BIT NOT NULL DEFAULT false, NotebookSyncTime TEXT,
NotebookSyncToken TEXT,
PRIMARY KEY (UserID)
)
NOT NULL columns: UserID, UserKey, IsLibraryMigrated (default true),
Subscription (default 0), NewUserPromoValue (default -1.0),
AnnotationsMigrated (default false). Everything else nullable.
2.2 Kobo eReader.conf — relevant keys #
| Key | Section | Purpose |
|---|---|---|
SideloadedMode=true |
[ApplicationPreferences] |
Skips OOBE on first boot only. Once setup is past, it no longer suppresses sign-in prompts. Also prevents library/sync from being called even when other prerequisites are met — do not use it if you want sync. |
api_endpoint |
[OneStoreServices] |
Base URL for sync calls. Override to point at self-hosted server (kobo-shelf, Komga, Calibre-Web). |
image_host, image_url_template, image_url_quality_template |
[OneStoreServices] |
Cover image URLs. Nickel reads these from cached Resources, not the conf — but a redundant override here survives firmware rewrites. |
oauth_host=https://oauth.kobo.com |
[OneStoreServices] |
OAuth refresh endpoint. Separate from api_endpoint. Token refresh hits this directly. Critical for account-less attempts. |
FW 4.39+ rewrites Kobo eReader.conf during sync/upgrade. Overrides get
reverted unless you also override image_host + URL templates so the rewrite
preserves them. Re-check the file after every FW bump.
Gotcha: after a real Kobo sign-in the device writes its own
api_endpoint=https://storeapi.kobo.com. You must dedupe — strip all
api_endpoint= lines, then insert yours once after the [OneStoreServices]
header. Two lines → last one wins → goes to Kobo.
3. Bypass methods (no Kobo account) #
3.1 Pre-OOBE bypass (cleanest, factory-fresh only) #
- Boot device → connect-to-WiFi prompt → don't connect.
- Plug into PC, mount.
- Delete
/.kobo/KoboReader.sqlite. - Append to
[ApplicationPreferences]inKobo eReader.conf:SideloadedMode=true - Eject, reboot → device drops into accountless sideloaded mode.
The OOBE "Skip" button was removed on Libra Colour — the file edit is the only way. Sources: qtekfun, MobileRead Libra Colour thread.
3.2 Post-OOBE bypass (setup already completed) #
If OOBE finished and the user table is cleared, SideloadedMode=true alone
isn't enough — Nickel sees "completed setup but no user" → sign-in prompt.
Insert a minimal user row:
-- option A
INSERT INTO user (UserID, UserKey, UserDisplayName, UserEmail)
VALUES (strftime('%s%f','now'), '1234', 'foo', 'foo@bar.com');
-- option B (sifr01 recipe)
INSERT INTO user(UserID, UserKey) VALUES('1', '');
Both work; keep all other columns NULL/default. Do not set AuthType,
AuthToken, RefreshToken — see §4.
3.3 The "fully fake" account row that lets sync fire (kobo-shelf recipe) #
For a factory-reset device with no real Kobo account, the minimal row is not
enough — firmware still shows the sign-in wizard. Populate enough of the row
that firmware treats it as signed-in and stays in sync mode (do not use
SideloadedMode=true — that disables sync):
UserID='-' -- not the wiki's '1'/'-'; see SYNC_HANDOFF
UserKey='kobo-shelf'
UserEmail=<something>
UserDisplayName=<something>
Storefront='US'
IsOneStoreAccount='true'
KoboAccessToken=<structurally-valid unsigned JWT>
KoboAccessTokenExpiry='2099-12-31T23:59:59.0000000Z'
The JWT is base64url({"alg":"none","typ":"JWT"}) . base64url({"sub":"kobo-shelf","exp":4102444800}) .
(trailing dot = empty signature). Firmware only null-checks these — no crypto
validation. A bare INSERT (UserID,UserKey) VALUES('-','-') is not enough;
the device calls our device_auth and stores our minted tokens regardless, but
that alone doesn't skip the wizard.
A fake_account.sh helper lives in the scratchpad (untracked).
4. Wipe traps — what triggers factory resets #
We hit "Critical error: All books, documents and custom settings will be deleted from this device" twice. Causes:
4.1 AuthType='AccountSignedIn' with fake AuthToken/RefreshToken #
Nickel runs a local validation on auth state at boot/plug events. With
AuthType='AccountSignedIn' it expects parseable tokens; fake strings like
'fake-auth' / 'fake-refresh' fail → "session changed, wipe".
The wipe fires without WiFi — no OAuth round-trip. It's a local consistency
check. Earlier hypothesis (OAuth refresh against oauth.kobo.com) was wrong;
confirmed by timing (wipe popped before any network joined).
Rule: don't set AuthType. Don't set AuthToken. Don't set RefreshToken.
Leave them NULL.
4.2 UserID mismatch with library/sync response #
If sync runs and the server returns a UserID differing from user.UserID,
Nickel decides "different account" and wipes. Komga's server-derived UserID is a
UUID; if your fake row uses '1' and Komga reports a different ID → wipe.
Fix: match UserID, or prevent sync from running (SideloadedMode for sideload-
only operation).
4.3 Other suspected (not directly observed) triggers #
___DeviceIDmismatch with the hardware-derived ID (computed from MAC/serial). Fake___DeviceID='baz'may trigger a "wrong device" wipe. Set NULL or match. Note: the sifr01 recipe sets___DeviceID=''and reportedly works — suggests empty is treated as "absent", not "wrong".- Cross-table consistency mismatches (book entitlements referencing a different UserID than the user row). Less likely on a clean DB.
4.4 Recovery from a wipe-warning popup #
Do not tap OK / Continue. Hold power 10+ s to force-off. Then:
- Plug into PC, mount.
DELETE FROM user;inKoboReader.sqlite.- Re-insert a minimal fake row (no
AuthType, no tokens). - In
Kobo eReader.conf: removeapi_endpoint=if you don't have a working sync endpoint; setSideloadedMode=true. - Eject, boot, recover.
If the wipe fully completed (factory reset): re-run the pre-OOBE bypass. FW
updates that auto-install during recovery may alter the user schema — re-check
before inserting.
5. Self-hosted sync paths — what works #
5.1 Native Kobo sync via Komga #
Komga implements /v1/initialization, /v1/auth/device, /v1/library/sync,
etc. at /kobo/<api_key>/.... Set
api_endpoint=https://komga.example.com/kobo/<api_key>. Kepubify converts
EPUB→KEPUB on the fly.
Works only with a real Kobo account first. Without an account, sync stops
after /v1/initialization because Nickel's library/sync path requires
IsOneStoreAccount=true in the user row, and adding that with fake tokens
triggers a wipe. No row shape satisfies both "sync fires" and "no wipe" without a
real account. (kobo-shelf later solved this — see §6.2.)
- Docs: https://komga.org/docs/guides/kobo/
- Issue: https://github.com/gotson/komga/issues/2083
- Release: https://github.com/gotson/komga/releases/tag/1.23.6
Benign warning WARN ... Failed to get response from Kobo /v1/initialization, fallback to noproxy always fires when "Proxy unknown requests to Kobo Store" is
off. It's a logging papercut — Komga throws IllegalStateException("kobo proxying is disabled") internally and catches it in the controller. Confirmed
in KoboProxy.kt:80 / KoboController.kt:198.
The Resources block returned by /v1/initialization is cosmetic. Komga
hardcodes storeapi.kobo.com URLs for everything except image_host,
image_url_template, image_url_quality_template. Kobo firmware ignores
those URLs and uses api_endpoint from Kobo eReader.conf for all sync calls.
(Verified against nativeKoboResources in KoboProxy.kt:148-341.)
5.2 Native Kobo sync via Calibre-Web #
Same shape — separate /kobo/<auth_token>/... endpoint. CBZ→EPUB conversion
handled externally (e.g. Kombo). Same
account-required constraint as Komga. Wire-format token uses
base64(json{"data":{"raw_kobo_store_token": ...}}) with no . separator.
5.3 Native Kobo sync via kobo-shelf (this project) #
Rust/axum server, reads a Calibre metadata.db read-only and maintains its own
SQLite app DB. Implements all major endpoints + sync-token logic + auth/device +
auth/refresh + library/state + tag CRUD. More features than Komga in places
(tag CRUD, statistics storage). Full no-account sync now works — see §6.
5.4 KOReader (third-party reader, no Nickel sync) #
Best path for accountless self-hosting if you don't need Nickel sync. Coexists with Nickel as an alternate boot target. Native OPDS, plugin ecosystem.
- Install: download
koreader-kobo-vYYYY.MM.zipfrom releases, extract directly to/mnt/onboard/(thekoreader/folder +koreader.pngappear at root). NotKoboRoot.tgzstyle anymore. - Launcher: NickelMenu — drop its
KoboRoot.tgzinto/.kobo/. Config at/mnt/onboard/.adds/nm/<name>:menu_item :main :KOReader :cmd_spawn :quiet :exec /mnt/onboard/koreader/koreader.sh - Plugin management: file browser → menu (≡) → wrench tab → Plugin management. The plugin's UI lives in different tabs (magnifier = content browsers, tools = utilities). Restart KOReader after enabling for menu changes.
lfsquirk: KOReader exposeslfsasrequire("libs/libkoreader-lfs"), notrequire("lfs"). PC-developed plugins using systemlfsneed patching.- OPDS catalogs:
koreader/settings/opds.luais a plain Lua table — direct edits work, no on-device typing.
Existing plugins (as of May 2026):
LK4D4/suwayomi_dl.koplugin— Suwayomi browser/downloader. Manual download, mark-as-read sync. Alpha; has thelfsbug.OGKevin/kobo.koplugin— Kobo native library browsing + Nickel-DB read state. Doesn't talk to Komga/Suwayomi.AhzeLeak/komgasync.koplugin— Komga sync prototype. Single commit, Windows-shell calls, broken on Kobo. Starting code only.hanatsumi/rakuyomi(archived) → forktachibana-shin/rakuyomi— manga reader, Aidoku-style sources, online reading.
No KOReader plugin currently does seamless background auto-pull from Komga or
Suwayomi. Closest would be hooking the NetworkConnected event.
5.5 Suwayomi-Server #
Self-hosted manga server. NixOS module: services.suwayomi-server. Default API
port 4567. Auth modes (server.conf): none, basic_auth, simple_login
(cookie), ui_login (JWT, but only mintable via the WebUI login flow — not
usable for headless clients). No static API key concept as of v2.1.x. For
headless clients, basic auth is the only practical option.
5.6 Authelia + Suwayomi behind Caddy (dual auth path) #
To run Suwayomi behind Authelia (browser) and let a non-interactive client (KOReader plugin) use Suwayomi's basic auth without interactive login, use Caddy with three matchers:
@api_browser {
path /api /api/*
header Cookie *authelia_session*
}
@api_plugin {
path /api /api/*
not header Cookie *authelia_session*
}
handle @api_browser {
import auth
reverse_proxy localhost:4567 {
header_up Authorization "Basic {$SUWAYOMI_BASIC_B64}"
}
}
handle @api_plugin {
reverse_proxy localhost:4567
}
handle {
import auth
reverse_proxy localhost:4567 {
header_up Authorization "Basic {$SUWAYOMI_BASIC_B64}"
}
}
Three buckets: /api + Authelia cookie (browser SPA XHR) → Authelia + inject
Suwayomi creds. /api without cookie (plugin) → forward as-is, plugin sends
basic auth. Non-/api (browser HTML) → Authelia + inject. {$VAR} substitutes
at parse time from the environment; systemd EnvironmentFile= provides the
base64 (load via sops template as Caddy's environmentFile).
6. The kobo-shelf store-bypass work — both phases done + device-verified #
A factory-reset Kobo with no real Kobo account now syncs the full library
and streams downloads entirely against kobo-shelf — no OAuth, no
authorize.kobo.com. Real-account devices also benefit: a full sync completes
with zero redirects to storeapi.kobo.com, so no library/reading/browsing
data leaks.
6.1 The original redirect fix (PR #1, commit e3bbaa2) #
handle_unimplemented now 307-redirects unimplemented store endpoints to
https://storeapi.kobo.com (via strip_kobo_prefix) instead of returning empty
200s. Root cause: empty-body 200 stubs made the device treat the store
session as broken and it aborted before ever calling /v1/library/sync.
Redirecting to real Kobo (calibre-web's proxy behaviour) keeps the device happy
so it proceeds to library sync, which kobo-shelf serves. (Later made toggleable via
the proxy_unimplemented runtime setting — see §11.)
6.2 Phase 1 — self-serve store endpoints [SHIPPED] #
src/kobo/handlers.rs + router.rs: self-served handlers for profile,
wishlist, recommendations, featured, deals, affiliate, assets,
subscriptions, prices, nextread, analytics_event. handle_unimplemented
is the router fallback (unlisted store paths still 307 to Kobo — safe).
- Real response shapes were captured from
storeapi.kobo.comusing the device'suser.AuthToken(theKoboAccessTokencolumn 401s — useAuthToken). Notable:nextread→{}(not[]);prices→{"Items":[]}. - Profile identity is synthetic and the device accepts it — US storefront,
UserId =
UUIDv3(auth_token), empty email, emptyPrivacyPermissions(kills all Kobo trackers). Overridable viaKOBO_SHELF_STORE_USER_ID/KOBO_SHELF_STORE_EMAIL/KOBO_SHELF_STORE_COUNTRY(config fields inconfig.rs). - Verified sync sequence (all 200, no redirects):
init → profile → benefits → subscriptions → deals → analytics/gettests → library/sync → wishlist → recommendations → nextread×N
6.3 Phase 2 — self-minted auth, no real account [SHIPPED] #
Two changes (PR #3 feat/self-host-auth):
src/kobo/resources.rs:build()overridesdevice_auth+device_refreshto{kobo_prefix}/v1/auth/device|refresh(now six overrides, was four).src/kobo/handlers.rs+router.rs: self-servePOST /v1/user/add-device→200. This was the final blocker — without it the device callsadd-deviceafterauth/device, the fallback 307s it to Kobo, the synthetic bearer is rejected, and the device stalls beforelibrary/sync.
Device-side recipe for a no-account device (factory reset):
- Set
api_endpointunder[OneStoreServices]inKobo eReader.conf. - Insert + populate a
userrow per §3.3 (do not useSideloadedMode). - Eject/unmount, unplug, reboot → boots to home → Sync.
Verified sequence (all 200, previously only add-device leaked):
init → ping → auth/device → init → affiliate → profile → benefits → subscriptions → deals → gettests → add-device → library/sync → metadata → covers → downloads
6.4 Phase 3 — activation bypass NOT NEEDED #
No-account sync works without it. The only path still redirected to Kobo in the
no-account flow was add-device (now self-served). Audit for stray redirects
only if you want a fully airtight no-leak setup. If ever needed, it would
require DNS redirect of Kobo auth domains + a TLS cert the device trusts (SSH /
root on the Kobo; the firmware snapshot in ../kobofw has ssh-disabled →
rename to ssh-enabled).
7. Runtime / operational setup #
Everything runs on the dev machine; the Kobo reaches it over a Tailscale funnel.
7.1 Server #
Needs the nix dev shell for the toolchain; the built binary runs standalone at
target/debug/kobo-shelf:
env KOBO_SHELF_HOST=127.0.0.1:8080 \
KOBO_SHELF_EXTERNAL_URL=https://ryu.lemur-newton.ts.net \
KOBO_SHELF_CALIBRE_LIBRARY=/volumes/media/Books \
KOBO_SHELF_APP_DB=/home/servius/Projects/kobo-shelf/kobo-shelf.db \
RUST_LOG=info,tower_http=debug \
./target/debug/kobo-shelf
.env is supported via dotenvy (main.rs) and gitignored — run with just
./target/debug/kobo-shelf from the repo root.
7.2 Tailscale funnel (public HTTPS ingress → local :8080) #
tailscale funnel --bg 8080 # enable
tailscale funnel --https=443 off # disable when done
- Funnel host:
https://ryu.lemur-newton.ts.net - Public ingress IP (from outside the tailnet):
dig +short @1.1.1.1 ryu.lemur-newton.ts.net - Testing the funnel URL from the dev box itself fails TLS (MagicDNS points at
the tailnet IP; funnel ingress rejects it). Test locally with:
curl --resolve ryu.lemur-newton.ts.net:443:<public-ip> https://...
7.3 Key facts #
- Calibre library:
/volumes/media/Books(14 books; 11 EPUB, 3 KEPUB). - App DB:
/home/servius/Projects/kobo-shelf/kobo-shelf.db. - Kobo auth token (URL path segment):
87b2d4ba-fc0f-4fee-90d3-732f21539715(row inauth_tokens,user_id=1). Sync URL base:https://ryu.lemur-newton.ts.net/kobo/87b2d4ba-fc0f-4fee-90d3-732f21539715 - Device account (real-account path):
books@darksailor.devsigned in on device.
7.4 Device configuration #
Mount point when plugged in: /run/media/servius/KOBOeReader.
.kobo/Kobo/Kobo eReader.conf→[OneStoreServices]must have exactly ONEapi_endpointline pointing at us:api_endpoint=https://ryu.lemur-newton.ts.net/kobo/<token>- Backups left on device:
Kobo eReader.conf.kobo-shelf.bak,KoboReader.sqlite.*.bak. - Always
sync+udisksctl unmount -b /dev/sdcbefore unplugging so edits flush.
The real-account path (what worked first): api_endpoint alone cannot
bypass Kobo's login — the device bootstraps auth against real Kobo
(authorize.kobo.com / oauth.kobo.com) before honoring api_endpoint. So:
sign in with a real Kobo account (free), then point api_endpoint at
kobo-shelf. Auth stays with Kobo (succeeds); kobo-shelf serves library sync + downloads.
This is now superseded for no-account setups by §6.3.
7.5 Environment gotchas #
sqlite3,cargo,cconly exist insidenix develop— prefix DB queries and builds withnix develop --command ....- Reading the auth token via
sqlite3 select auth_token ...is blocked by the permission classifier (it's a credential). Inject into files without printing, or use the known value above. - Background server dies on every Claude session restart (the sandbox reaps
detached processes;
setsid/&get killed with exit 144). Use the harness- managed background task and restart after a session boundary. For a durable setup use the NixOS module (commitd860888) to run kobo-shelf as a systemd service. - Server logs: managed-task output file, or
/tmp/kobo-shelf.logif launched withtee. Strip ANSI before grepping:sed -r 's/\x1b\[[0-9;]*m//g'. - AGENTS.md rule: never change the Kobo API wire contract;
cargo fmt --check+cargo clippy --all-targets -- --deny warningsmust pass; nounwrap/expect.
8. Every endpoint the device calls (observed) #
Implemented + working: initialization, library/sync,
library/{uuid}/metadata, library/{uuid}/state (GET/PUT), cover images
(/{uuid}/{w}/{h}/{q}/isGreyscale/image.jpg), download/{id}/{fmt}, ping,
auth/device, auth/refresh, user/add-device.
Self-served stubs (Phase 1): user/profile, user/wishlist,
user/recommendations, user/loyalty/benefits, deals, affiliate, assets,
products/featured/, products/{uuid}/nextread, products/{uuid}/prices,
products/books/subscriptions, analytics/event, analytics/gettests.
Fallback: any other /kobo/{token}/... store path → handle_unimplemented
307-redirects to storeapi.kobo.com (toggleable via proxy_unimplemented).
Real shapes worth remembering: nextread → {} (not []); prices →
{"Items":[]}; wishlist/deals/subscriptions → {"Items":[]};
recommendations → []; affiliate/assets → {}; benefits →
{"Benefits":{}}.
9. Sync algorithm #
9.1 Current kobo-shelf design [SHIPPED — SyncPoint refactor] #
kobo-shelf now uses Komga's snapshot-diff model (the refactor described in the old
Komga findings has landed — src/kobo/sync_point.rs exists). Token holds
snapshot IDs; each sync freezes a per-call snapshot of the library state and
diffs from → to. This gives:
- Race-free pagination — library mutations during a multi-page sync don't
corrupt the cursor (
tois frozen at snapshot time). - Correct deletions — "in
from, not into" yields an exact removed set (no reliance on a sidearchived/shelf_archivesflag). - File-content changes detected via hash + mtime + size.
- Force-sync = delete the user's
last_successful_sync_point_idrow.
Token wire format: KOBO_SHELF.<base64(json)> in the x-kobo-synctoken header
(see src/kobo/sync_token.rs). Bare values without the prefix are treated as a
raw upstream Kobo token.
9.2 Known diff-emission rule (already shipped) #
When emitting a ChangedEntitlement, also emit a paired ChangedReadingState
— Kobo ignores the ReadingState embedded in ChangedEntitlement, so progress
was otherwise silently dropped. On PUT state with status=Finished, the
device sends a bogus locator pointing at the first resource; kobo-shelf ignores it
and forces progress to 100% to keep the last good location.
9.3 Token-format compatibility [PENDING — small win] #
Komga's generator transparently accepts Kobo store tokens (contain .) and
Calibre-Web tokens (no ., JSON with data.raw_kobo_store_token). kobo-shelf
currently only recognizes the KOBO_SHELF. prefix and a bare raw Kobo token — it
does not parse Calibre-Web tokens. Adding CW compat is ~30 LOC and a UX win
for migrators. Reference impl: KomgaSyncTokenGenerator.kt.
10. Komga feature comparison & adoption scoreboard #
| # | Item | LOC | Risk | Reward | Status |
|---|---|---|---|---|---|
| 1 | Port-fix middleware | ~50 | low | high — silently breaks cover/download URLs behind some NATs | [PENDING] |
| 2 | kepubify integration | ~200 | medium (subprocess, temp files) | very high — real per-page progress on EPUBs | [SHIPPED] (upload/convert.rs, KOBO_SHELF_KEPUBIFY_PATH) |
| 3 | SyncPoint refactor | ~600 + migration | high | medium-high — correctness on busy libraries | [SHIPPED] (kobo/sync_point.rs) |
| 4 | Store proxy (merged) | ~300 | high (privacy, auth forwarding) | medium — only users with Kobo store accounts | [PARTIAL] — redirect-to-Kobo fallback exists (proxy_unimplemented); full merge-proxy not implemented |
| 5 | Calibre-Web token compat | ~30 | low | low-medium — UX for migrators | [PENDING] |
| 6 | Cover format conversion (→ JPEG) | ~50 | low | low-medium — fixes JPEG-only firmwares | [PARTIAL] — uploads transcode (upload/cover.rs); Kobo cover endpoint always serves image/jpeg regardless of source |
| 7 | Per-device token comment column |
~10 | trivial | low — revoke a single eReader | [PENDING] (no comment column) |
10.1 Port-fix middleware (item 1, still open) #
Some Kobo firmwares send Host: without a port even when connecting on a
non-standard one. Spring/axum then build URLs with port 80 → broken cover /
download URLs returned to the device.
Komga's KoboMissingPortFilter is a OncePerRequestFilter that:
- Skips when any
Forwarded/X-Forwarded-*header is present (let the reverse proxy win). - Otherwise wraps the request so
getServerPort()returns a configuredkoboPortSupplier()value.
Cost for kobo-shelf: axum from_fn middleware, ~30 LOC, run only on /kobo/*.
Read Host; if no port → rebuild URI with config.kobo_external_port before
the request reaches handlers. Skip when any X-Forwarded-* is present. Also:
Komga relaxes Tomcat's query-character set to allow [ ] (Kobo violates RFC
3986 in /v1/assets?DiffRequests=[…]). axum/hyper is more permissive — verify
with a real request before assuming a workaround is unneeded.
10.2 Full store merge-proxy (item 4, mostly unneeded now) #
Phase 1 + 2 made the redirect-to-Kobo fallback unnecessary for the common
flows. The merge-proxy is still relevant only for users who want purchased
Kobo books to keep syncing alongside local books. It relays unknown calls to
storeapi.kobo.com, merging the store's sync results into kobo-shelf's response
and rewriting the sync-token header in both directions so the device sees one
merged token.
Concerns: egress/privacy (device traffic relayed through us to Kobo — must be
opt-in); auth bleed (real Kobo Authorization forwards verbatim; user must
have signed into a real account). Implementation: reqwest client + header
filtering + catch-all axum route under /kobo/{token}/{*rest} + the native
resources JSON. Config flag default off. Reference: KoboProxy.kt.
11. Runtime settings & per-user book sync #
Two things are user-toggleable at runtime from the web Settings page
(AppConfig stays startup-only): a key/value settings table holds
proxy_unimplemented (whether handle_unimplemented 307-redirects unknown
store paths to storeapi.kobo.com or returns 404) and sync_new_uploads
(default sync flag stamped on new uploads).
Per-book sync is per-user: books.default_sync is the book's default and
book_sync_prefs(user_id, book_id, enabled) overrides it; a user's syncable
set is COALESCE(pref, default_sync)=1, filtered in
BookStore::fetch_syncable_books_with_format(user_id) so a toggled-off book
drops out of that user's next sync-point snapshot (emitted as removed).
12. OPDS #
- Komga:
https://<host>/opds/v1.2/catalog(v2 also at/opds/v2/catalog). - Calibre-Web:
https://<host>/opds. - Suwayomi: experimental OPDS at
/api/opds, less complete. - KOReader stores catalogs in plain Lua (
koreader/settings/opds.lua); each entry hastitle,url, optionalusername, optionalpassword. Direct file edit works.
13. Open questions / untested claims #
Stated during research but not personally verified — flag for skepticism.
- Can sync fire with
IsOneStoreAccount=truebut noAuthType/tokens? The §6.3 no-account recipe effectively answers "yes" for kobo-shelf (it populatesIsOneStoreAccount+ a structurally-valid unsigned JWT, noAuthType). The narrower "onlyIsOneStoreAccount=true, everything else NULL" combo remains untested in isolation. - Does
___DeviceID='fake'trigger a wipe directly, or only withAuthType? The sifr01 recipe sets___DeviceID=''and reportedly works — suggests empty is treated as "absent", not "wrong". - Does overriding
oauth_hostto a self-hosted target work? No test data. Plausible based on protocol but FW 4.45+ may pinoauth.kobo.com's cert. - Does FW 4.45 require new conf overrides beyond 4.42-era guides? Observed rewrites kept our overrides intact, but the schema changed (29 vs 27 cols).
14. Process lessons #
- Backup
KoboReader.sqlitebefore each DB edit. Wipes are unrecoverable. - Test changes USB-mounted with the device powered off, then boot — not while on WiFi. Removes one variable.
- Don't tap "OK" on any unfamiliar dialog. Hold power, force off, plug in, investigate.
- The same fake-row schema may behave differently across FW versions. Re-verify after every auto-update.
- Komga's "Proxy unknown requests to Kobo Store" should be off for accountless setups. The benign warning still fires.
- KOReader's
lfsrequire quirk (libs/libkoreader-lfs) catches PC-developed plugins. Always check forrequire("lfs")in third-party plugin code.
15. Reference URLs #
Kobo device / bypass
- qtekfun bypass guide
- MobileRead — Libra Colour skip-registration
- MobileRead — original fake-registration
- MobileRead — api_endpoint reverting
- sifr01 wiki — kobo without registration
- pgaskin Kobo firmware index
- Kobo Touch Hacking — MobileRead wiki
KOReader / NickelMenu
Komga / Calibre-Web / Kombo
- Komga Kobo Sync guide
- Komga source — KoboController.kt
- Komga issue #2083 — disable proxy fix
- Calibre-Web Kobo plugin source — kobo.py
- Calibre-Web-Automated wiki — Kobo Integration
- Tiduj/Kombo — CBZ→EPUB for Kobo sync
Suwayomi
16. Komga source file reference index #
All paths under komga/src/main/kotlin/org/gotson/komga/.
| File | Purpose |
|---|---|
interfaces/api/kobo/KoboController.kt |
Controller — every Kobo endpoint (~840 LOC) |
infrastructure/kobo/KomgaSyncTokenGenerator.kt |
Sync-token encode/decode; accepts Kobo store + CalibreWeb tokens |
infrastructure/kobo/KoboProxy.kt |
Store proxy + native /v1/initialization resources JSON (~150 entries) |
infrastructure/kobo/KepubConverter.kt |
kepubify subprocess wrapper |
infrastructure/kobo/KoboHeaders.kt |
Header name constants |
infrastructure/web/KoboMissingPortFilter.kt |
Port-fix servlet filter |
infrastructure/jooq/main/KoboDtoDao.kt |
jOOQ queries for sync DTOs |
domain/model/KomgaSyncToken.kt |
Sync-token data class |
domain/model/SyncPoint.kt |
Snapshot row + nested Book / ReadList entities |
domain/service/SyncPointLifecycle.kt |
Snapshot creation + diff queries |