A private library for every book you own. EPUB, PDF, comics and audiobooks — organised, readable, and fully yours. Optional AI reading guides and narration that can run entirely on your own hardware. No cloud account. No subscription.
One container + Postgres · OPDS 1.2 · reMarkable · send-to-Kindle
Not a sync service you tolerate. A home you actually want to use.
EPUB, PDF, comic and audiobook readers, all in the browser. One resume token per book covers every format — CFI for EPUB, page for PDF, timestamp for audio — so you pick up where you left off whichever way you were consuming it.
Covers, descriptions, series and ratings streamed in over SSE as each provider answers. Per-field padlocks keep automation off the fields you curated by hand, and every edit is written back to a JSON sidecar and a Calibre-compatible OPF.
Rule-based shelves that populate themselves — "essays rated 4★+", "unread, added this year" — alongside hand-curated ones. Both get an accent colour and a Lucide icon.
Watch a folder, drag files onto the page, or point it at an S3 prefix. embookshelf extracts metadata and a cover, then queues the file for your approval before anything enters the library.
An LLM-written orientation per book: what it is about, who it suits, who should skip it. Any OpenAI-compatible endpoint works, so a local Ollama keeps every book on your own hardware. Off by default; generation is always a button someone presses.
Read an EPUB aloud and save it beside the book as an MP3 with chapter marks. OpenAI-compatible, ElevenLabs or Azure Speech — or a local Kokoro for free. Admin-only, one book at a time, priced in front of you before the run starts.
An optional sidecar image that turns a PDF into Markdown — and from there into a generated EPUB. It classifies the PDF first and refuses scanned or sparse files loudly instead of producing nonsense. Everything else works without it.
Per-user shelves, progress, annotations and preferences. OIDC for Google, GitHub or your own IdP with PKCE on every flow, plus forward auth for Authelia, oauth2-proxy, Traefik and Cloudflare Access. Admin invites and password reset ride the SMTP transport.
Every byte of every book flows through one storage seam. A library is backed by the filesystem or by S3 — MinIO, R2, B2, AWS — with presigned delivery, and edit-time renames copy first so links issued a minute ago still resolve.
Most digital libraries are EPUB-centric. embookshelf ingests nine file types — metadata, cover and all — and reads four of them in the browser with a dedicated reader each. The rest download and serve over OPDS.
The embookshelf reader uses Literata — commissioned for Google Play Books — as its default reading face. Margins, line height, font size, and theme are saved per-user.
The village had not been on any of her grandfather's maps. Ellen could not at first decide whether this was because he had missed it, or because it had not yet existed when he last passed through.
She walked down what she took to be the main street, pulling her coat around her. It was not yet evening but the light had already begun to fail in the way it did only this far north.
"You'll be the one looking for the house," a woman said from behind her.
Reading guides, narration and PDF conversion are all off by default and all configured in the admin UI — not by env var — so credentials are encrypted at rest and change without a restart. Point them at a local model and nothing leaves your machine.
Base URL, model, credential, guide language, and how much book text to send. Any OpenAI-compatible endpoint — Ollama, vLLM, OpenAI, whatever answers the shape. Run it per book, or as an admin run over the library that shows a token estimate first.
One engine — OpenAI-compatible, ElevenLabs or Azure Speech — with a key, a model and a voice. Load voices lists what the engine really offers. EPUB only; the MP3 lands beside the book with chapter marks a podcast player can read.
A separate image on the compose-internal network. Point the panel at http://converter:6070 and a PDF gains a Markdown rendition — which is what a reading guide for a PDF feeds on — plus an optional generated EPUB.
The UI is embedded in the Go binary — no Node runtime in production. Postgres is the only external dependency, and the bundled compose file brings its own. Nothing phones home unless you turn OpenTelemetry on.
# The whole stack: app + Postgres services: embookshelf: image: ghcr.io/blackforgehq/embookshelf:latest ports: ["127.0.0.1:6060:6060"] environment: DATABASE_URL: postgres://…@postgres-embookshelf:5432/embookshelf APP_URL: https://books.example.com EMBOOKSHELF_SECRET_KEY: ${EMBOOKSHELF_SECRET_KEY} volumes: - embookshelf_data:/data - embookshelf_bookdrop:/bookdrop postgres-embookshelf: image: postgres:16-alpine # docker compose -f compose.prod.yml up -d
Clone the repo for the compose file, or lift the excerpt above into your own stack. Schema migrations apply on boot.
POSTGRES_PASSWORD=$(openssl rand -hex 24) APP_URL=https://books.example.com SESSION_SECRET=$(openssl rand -hex 32) EMBOOKSHELF_SECRET_KEY=$(openssl rand -base64 32)
EMBOOKSHELF_SECRET_KEY: it decrypts every stored credential, and it belongs in your backups alongside the database.docker compose -f compose.prod.yml up -d
--profile converter to also start the PDF-to-Markdown sidecar, then point Settings → Converter at http://converter:6070.APP_URL becomes the admin. Then create a library in Settings → Libraries — one root per library, fixed at creation, local disk or S3.make db-up # start Postgres make ui-install # bun install in ui/ make converter-up # optional sidecar on :6070
make up # Go on :6060 + Vite on :5173
http://localhost:5173. Vite proxies /api, /opds and /events to the Go server, so cookies and SSE keep flowing. make seed gives you admin@local / changeme.make build && ./tmp/embookshelf
internal/staticfs/dist and embedded — the result ships without Node or Bun.embookshelf has been Postgres-only since ADR-0023, and this release refuses to boot on a sqlite:// DSN. Migrate in one command, into an empty database:
DATABASE_URL='postgres://user:pass@host:5432/embookshelf' \ embookshelf import-sqlite --from ./data/embookshelf.db
Queued background jobs don't transfer — trigger a library scan afterwards.
Metadata providers, OIDC, SMTP, reading guides, audiobooks and the converter are configured in the admin UI and stored in app_settings — changeable without a restart, encrypted at rest. The env vars below are the ones the process needs before it can serve a request. All are optional unless noted.
sqlite:// DSN now refuses to boot.EMBOOKSHELF_SECRET_KEY is the only variable they care about — it encrypts the keys you paste in.EMBOOKSHELF_S3_BOOKDROP_PREFIX and objects written to that prefix are staged like local drops. Polls once a minute by default; every tick is a billable LIST.No proprietary client to install. The catalog speaks OPDS, and files are files.
"Reading is the activity. Managing is the overhead. The UI should feel like the activity."embookshelf Design PRD · 2026

Open source. Self-hosted. AGPL-3.0. Your data stays on your server.
docker pull ghcr.io/blackforgehq/embookshelf:latest