v0.6.7 · Self-hosted · Open source · AGPL-3.0

Books you own,
shelved your way.

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

The Cartographers of Dusk
Instruments
The Pigeon Courier
The Unmade Bed
Slow Weather
Below the Watermark
A History
Unbound
Field Notes
Seventeen Winters
The Salt Ledger
Household Gods
Radio Static
Orchard
Tidewater
Almanac for the Sleepless
The Glass Apiary
Letters
4
Built-in readers
EPUB, PDF, comics, audiobook
6
Metadata providers
Google Books, Open Library, Hardcover, Goodreads, Amazon, DuckDuckGo
3
Auth methods
Local sessions, OIDC / SSO, reverse-proxy headers
1×
Container
Single image plus Postgres — nothing else required

Everything a reading library should be.

Not a sync service you tolerate. A home you actually want to use.

Built-in readers

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.

Metadata, six providers

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.

Smart shelves

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.

BookDrop ingest

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.

Reading guides

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.

Audiobook narration

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.

Converter extension

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.

Multi-user & SSO

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.

Local disk or S3

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.

Every format
you actually have.

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.

· · ·
EPUBEPUB 2 & 3Paginated · TOC · CFI resume
PDFPDFpdf.js · → Markdown & EPUB
CBZCBZ · CBR · CB7ComicInfo.xml · manga mode
M4BMP3 · M4A · M4BChapter nav · range requests
MOBIMOBI · AZW3 · FB2Metadata + cover · download / OPDS

A reading experience that respects the text.

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.

  • Drop cap, hanging punctuation, justified text
  • Table of contents sidebar, highlights, margin notes
  • Chrome fades after 2 seconds — text only
  • Reading sessions feed a stats heatmap, live over SSE
  • A narrated EPUB gets a Read / Listen switch, same book
embookshelf.local/read/b1
Chapter Seven
The Mapmaker's Return

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.

p.138
33%

Optional AI,
on your own hardware.

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.

Every panel has a Test connection button that spends one short request, so a wrong key surfaces now rather than forty minutes into a run.
Reading guidesSettings → Reading guides

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.

Audiobook narrationSettings → Audiobooks

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.

Converter sidecarSettings → Converter

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.

One image,
one database.

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.

Docker Compose
Recommended · app + Postgres, behind your proxy
Existing Postgres
docker run with DATABASE_URL
Converter profile
--profile converter · optional sidecar
Single binary
make build → ./tmp/embookshelf
compose.prod.yml (excerpt)
# 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
The app image is distroless/nonroot (uid 65532) and binds to loopback, so your reverse proxy is the only thing exposed.

From nothing to a shelf, in four steps.

Clone the repo for the compose file, or lift the excerpt above into your own stack. Schema migrations apply on boot.

Production
  1. 1
    Write a .env next to the compose file
    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)
    All four are required — the compose file refuses to start without them. Keep EMBOOKSHELF_SECRET_KEY: it decrypts every stored credential, and it belongs in your backups alongside the database.
  2. 2
    Bring the stack up
    docker compose -f compose.prod.yml up -d
    Add --profile converter to also start the PDF-to-Markdown sidecar, then point Settings → Converter at http://converter:6070.
  3. 3
    Open the app and claim the admin account
    First signup at your APP_URL becomes the admin. Then create a library in Settings → Libraries — one root per library, fixed at creation, local disk or S3.
  4. 4
    Drop books in
    Copy files into the bookdrop volume, drag them onto the BookDrop page, or write them to an S3 prefix. They surface for review; approving moves the file into the library.
Development
  1. 1
    Prerequisites
    Go 1.25, Bun 1.x, Docker.
  2. 2
    One-time setup
    make db-up        # start Postgres
    make ui-install   # bun install in ui/
    make converter-up # optional sidecar on :6070
  3. 3
    Iterate
    make up   # Go on :6060 + Vite on :5173
    Open 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.
  4. 4
    Build the single binary
    make build && ./tmp/embookshelf
    The SPA is compiled into internal/staticfs/dist and embedded — the result ships without Node or Bun.
Coming from a SQLite install?

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.

Env for the machine. UI for everything else.

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.

Server / database
DATABASE_URL
postgres://localhost:5432/embookshelf
Postgres is required (ADR-0023). A sqlite:// DSN refuses to boot and points at import-sqlite.
APP_URL
request origin
Public origin. Feeds the OIDC redirect URI and the email links.
EMBOOKSHELF_SECRET_KEY
unset — dev only
Base64 32 bytes. AES-256-GCM for provider keys, OIDC secrets, the LLM key and TTS keys at rest.
EMBOOKSHELF_PORT
6060
HTTP listen port.
ALLOWED_ORIGINS
*
CORS / CSRF allow-list checked against Origin and Referer.
MIGRATE_ON_START
true
Apply the app schema and the River job-queue migrations on boot.
BOOKDROP_PATH
./bookdrop
Watched folder — the only ingest path (ADR-0018).
DATA_PATH
./data
Covers and on-disk caches.
S3 storage — only for kind=s3 libraries
EMBOOKSHELF_S3_BUCKET
unset
Shared bucket. Empty disables creating kind=s3 libraries.
EMBOOKSHELF_S3_ENDPOINT
unset
Custom endpoint for MinIO, R2, B2, Wasabi. Scheme optional.
EMBOOKSHELF_S3_FORCE_PATH_STYLE
false
Path-style addressing — required by MinIO and some R2 setups.
EMBOOKSHELF_S3_BOOKDROP_PREFIX
unset
S3 drop zone: objects under this prefix are staged into BookDrop, then deleted from the bucket.
EMBOOKSHELF_PRESIGN_TTL
10m
Presigned URL lifetime for S3-backed downloads.
EMBOOKSHELF_PRESIGN_FALLBACK
stream
Set to presign for 302 redirects. Presign needs bucket-side CORS for the app origin.
EMBOOKSHELF_S3_RENAME_GRACE
max(2 × TTL, 1h)
Delay before the orphan sweeper deletes old keys after an edit-time rename (ADR-0005).
OpenTelemetry — off unless you ask for it
OTEL_ENABLED
false
Server traces, metrics and logs over OTLP. Off means no telemetry leaves the box.
OTEL_EXPORTER_OTLP_ENDPOINT
unset
Collector endpoint. Protocol via OTEL_EXPORTER_OTLP_PROTOCOL (grpc).
VITE_OTEL_ENABLED
false
Browser SDK opt-in. Build-time, so it only applies to a UI you build yourself.
What changed recently
  • Postgres only. The dual-dialect machinery, FTS5 search and the polling queue are gone; jobs run on River inside the same database. A sqlite:// DSN now refuses to boot.
  • AI panels replaced env vars. Reading guides, audiobooks and the converter each live in Settings. EMBOOKSHELF_SECRET_KEY is the only variable they care about — it encrypts the keys you paste in.
  • S3 BookDrop. Set 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.
  • Comics are readable, not just shelved. CBR and CB7 join CBZ in the web reader, and FB2, MOBI and AZW3 ingest with metadata and covers.

Your books travel
on open protocols.

No proprietary client to install. The catalog speaks OPDS, and files are files.

OPDS 1.2
Basic Auth catalog — KOReader, Moon+, FBReader, Marvin, Aldiko
reMarkable Paper Pro
Pair with a one-time code, push from the book page
Send to Kindle
Your SMTP transport, EPUB and PDF
Planned
Kobo sync, KOReader progress — driver interface is in place
"Reading is the activity. Managing is the overhead. The UI should feel like the activity."
embookshelf Design PRD · 2026

Ready to shelve
your collection?

Open source. Self-hosted. AGPL-3.0. Your data stays on your server.

docker pull ghcr.io/blackforgehq/embookshelf:latest