KoBuddy
A self-hosted reading-stats dashboard for my e-reader. A KOReader plugin pulls my reading sessions off the device and syncs them to a dashboard I built and use daily — lifetime totals, heat maps, current reads, and goals — no cloud service in the loop. Heavily inspired by the open-source KoInsight project; built from scratch.
The problem
KOReader tracks rich reading stats in a local SQLite DB on-device — but the data is trapped there with no good way to see it.
Scratch-my-own-itch, made shareable: built for myself first, then open-sourced and self-hostable (Docker/Railway, a downloadable plugin, admin auth, a public-read toggle).
Credit up front: the idea and the plugin→server→dashboard shape are inspired by KoInsight; kobuddy is an independent from-scratch build, slimmer in scope, on a modern stack.
What I built
- KOReader plugin (Lua) — a "Sync reading stats" action plus optional auto-sync on device suspend, and a config screen for server URL + token.
- Self-hosted backend (Hono/Drizzle/SQLite) — ingest endpoints, stats aggregation on read, session-based admin auth, a public-read toggle, and OpenAPI docs.
- Dashboard (React/TanStack/Tailwind) — a bento-card home (lifetime totals, reading heat map, hourly chart, week/goal cards, current-book card, bookshelf with cover art from Open Library / Google Books), a paginated books view, and an admin view to edit metadata/covers/goals.
- Deployable by anyone — a Docker image, Railway config, and a downloadable plugin.zip.
How it works
Stack — a Lua KOReader plugin → a TS backend on Hono with Drizzle over SQLite (better-sqlite3), Zod-validated payloads, iron-session auth → a React 18 + TanStack Router/Query + Tailwind dashboard. pnpm + Turbo monorepo; Docker on Railway.
Architecture (plugin ↔ device ↔ dashboard) — KOReader keeps reading sessions in an on-device statistics.sqlite3 → the Lua plugin reads that DB and POSTs books + page-level sessions (bearer-token auth) → the backend upserts into its own SQLite via Drizzle and computes aggregate stats on read → the React dashboard fetches /api/stats + /api/books. A direct SQLite-file upload path exists for bulk initial import.
KOReader locks my reading data in an on-device SQLite file; I built a plugin-to-dashboard pipeline to liberate it, deployed it, and I use it daily. Conception, end-to-end integration, and deploy — mine regardless of who wrote the lines.
Under the hood
How the two hardest pieces work, under the hood:
The Lua plugin reads KOReader's internal SQLite directly: it flushes pending stats first, maps columns by index, and reconciles the DB's stored page count against the live repaginated count when the book is open (font-size changes repaginate) — pulling reading data straight out of the device.
Dedup on (device, book, page, start_time) with CASE logic so a flaky or partial sync never overwrites good maxima — the reliability spine.