- Go 82.5%
- HTML 10.2%
- CSS 6.5%
- JavaScript 0.4%
- Shell 0.4%
| design | ||
| docs | ||
| internal | ||
| scripts | ||
| .gitignore | ||
| Basefile | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
books
Personal book server with a Kobo sync endpoint. Imports EPUBs from an inbox directory, stores them in a library, exposes a web UI over OIDC, and pretends to be the Kobo store so a stock-firmware Kobo will treat the shelved subset as "purchased" books and download them.
Status: used in production for one user and one device (a Libra Colour on firmware 4.45.23646). Everything else is speculative.
Running the server
The books binary reads config from environment variables. Minimum set:
BOOKS_DATA_DIR=/opt/books # sqlite db, inbox/, library/
BOOKS_LISTEN_ADDR=0.0.0.0:8090
BOOKS_PUBLIC_URL=https://books.example.com
BOOKS_OIDC_ISSUER=https://auth.example.com
BOOKS_OIDC_CLIENT_ID=...
BOOKS_OIDC_CLIENT_SECRET=...
The first successful OIDC login claims ownership of the server. Subsequent logins are treated as regular users.
BOOKS_PUBLIC_URL is used as the base for every URL the Kobo is
handed (api_endpoint, download URLs, covers). It must be publicly
reachable from the device.
Build:
CGO_ENABLED=0 go build -o books .
modernc.org/sqlite is pure-Go, so CGO is off and the binary is
statically linked. It runs fine on Alpine / musl.
Pointing a Kobo at the server
One-time: register the device on the web UI
- Sign in to the books web UI.
- Go to Shelf. Under "your kobo", enter a name and click Register.
- Copy the generated URL — it looks like
https://books.example.com/api/kobo/<token>. It's per-device and acts as the device's credential; anyone with the URL can sync.
Edit Kobo eReader.conf over USB
The setup changes one line in the Kobo's config file. It does not need SSH, a factory reset, or any other Kobo-side modification. Nickel will rewrite every other URL it needs onto the new api_endpoint host automatically on next sync.
-
Plug the Kobo into a computer via USB. It appears as a mass-storage volume called
KOBOeReader. -
Open
.kobo/Kobo/Kobo eReader.confin a text editor. -
Under the
[OneStoreServices]section, replace the lineapi_endpoint=https://storeapi.kobo.comwith
api_endpoint=https://books.example.com/api/kobo/<token>where
<token>is the value from the web UI registration. -
Save the file, eject the volume, and reboot the Kobo.
On next sync the device will talk to books. Every other URL in
[OneStoreServices] still points at storeapi.kobo.com, but Nickel
rewrites the host at request time, so Cover images, metadata lookups,
device auth, analytics, and the book download all land on the books
server.
Verifying it worked
From the Kobo home screen, open Settings → Accounts → Sync now. In the books web log you should see:
GET /api/kobo/<token>/v1/initialization
POST /api/kobo/<token>/v1/auth/device
GET /api/kobo/<token>/v1/library/sync
...
and the device download flow (visible in the web log or the
packetdump.debug Qt channel on the Kobo, see
docs/kobo-api.md).
Internals and protocol notes
See docs/kobo-api.md for the full reference:
endpoint-by-endpoint wire shapes, protocol conventions, known failure
modes, and the nc :5001 debugging channel.
Admin API
A token-authenticated JSON API at /api/admin/* for editing book metadata
(search, PATCH fields, replace authors, rename authors/series, replace
cover/file). See docs/superpowers/specs/2026-04-13-admin-api-design.md for the
prose spec and docs/admin-api.openapi.yaml for the OpenAPI 3.1 schema. Authenticate with
Authorization: Bearer $BOOKS_ADMIN_TOKEN; if the env var is unset the
entire mount returns 404.
Frontend
Server-rendered HTML with htmx for interaction polish (shelve toggle,
library search). htmx is vendored at design/htmx.min.js (version
recorded in design/htmx.min.js.version). To bump: download the new
release artifact from https://github.com/bigskysoftware/htmx/releases,
overwrite both files, and verify the existing tests pass.