- Go 82%
- HTML 10.6%
- CSS 6.9%
- JavaScript 0.5%
Long descriptions (>1000 chars) are clamped with a one-way reveal: the clamped paragraph itself is the click target via a hidden checkbox + sibling selector; after expansion pointer-events are disabled so it can't be collapsed. Clamp uses max-height + a mask-image fade (not -webkit-box, which would break ::first-letter in Safari). Dropcap on the first letter of the description: float-based, font-size 5rem × line-height 0.95 so the cap spans exactly three body lines, with padding-right to give the body copy breathing room. initial-letter was tried and reverted because Safari's implementation ignores margin/padding. Also: drop the .book-marginalia top border (the progress rule above it already serves as the divider) and collapse the marginalia's top margin so the rhythm stays consistent. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| design | ||
| docs | ||
| internal | ||
| .gitignore | ||
| 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=...
BOOKS_OIDC_ALLOWED_SUB=... # the one user allowed to sign in
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-findings.md).
Internals and protocol notes
See docs/kobo-api-findings.md. It has
the full list of every Nickel quirk we've found, the nc :5001 live
debug procedure, and the wire format for each endpoint — in
particular the gotcha that /v1/library/{id}/metadata must be a
JSON array, not a bare object.
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.