Skip to content

Reading and OPDS

Two ways to read: the built-in web reader, and any OPDS client pointed at the server.

The web reader

Open any comic and press Read, or hover a cover in the grid.

Navigate click either edge, arrow keys, space, Page Up/Down, Home/End
Fit height, width, whole page, or actual size — remembered per browser
Spread s for two-page spreads, which always start on an even page
Direction d to flip; set automatically from ComicInfo's Manga field
Full screen f
Leave Escape

Progress saves as you read and the reader resumes there next time; reaching the last page marks the comic read.

Pages are served as URLs, not streamed by the application. The reader asks /api/comic/{id}/reader for a manifest and fetches the page URLs in it. Pages extract once into the page cache and are served from there, so turning back a page costs nothing and a PDF page is rendered once rather than on every view.

That indirection is the point: those URLs can move to a CDN without the reader changing. See Architecture, sections 3A and 3B.

The page cache is capped (LONGBOX_PAGE_CACHE_MB, 2 GB by default) and evicts whole comics, least-recently-read first, so a half-cached issue never stalls mid-way. Nothing in it is worth backing up or migrating — it rebuilds on demand.

The reader hardcodes a dark surround regardless of your theme, so artwork is never framed in white.

EPUB is the exception

EPUB is reflowable text rather than page images, so the reader turns it away with an explanation and offers the download instead. Rendering its spine documents in an iframe is a separate job, still on the roadmap.

OPDS

Longbox serves an OPDS catalogue, so phones and e-readers can browse and download over the network rather than only through this UI.

Both versions of the protocol ship:

Feed Version Format
/opds OPDS 1.2 Atom XML
/opds/v2 OPDS 2.0 JSON

The path decides the version. Accept only breaks a tie the path leaves open: a client naming application/opds+json and not Atom gets JSON from /opds, one naming both gets the path's answer, and /opds/v2 never degrades — there is no older version a 2.0 URL could have meant.

What is in the feed

/opds is the root, with navigation feeds beneath it:

Path
/opds/comics everything, paginated
/opds/series by series
/opds/publishers by publisher
/opds/years by year
/opds/search.xml an OpenSearch description

Assets sit once under /opds and are shared by both feeds — the file, the cover, individual pages and the Divina manifest. A CBZ is not a document with a version, and two URLs for one byte stream would mean two cache entries in every client and proxy that stores them.

Page streaming

Both versions offer the same pages two ways, because readers implement one or the other and rarely both:

  • OPDS-PSE — a URL template and a page count, fetched lazily. This is the one that works today: Panels uses it, including for resume.
  • A Divina manifest — a document naming every page up front, for Readium-based clients. No reader has opened one of Longbox's yet; Thorium, the obvious candidate, cannot consume a manifest from a remote server. See below.

Signing in from a reader

Two ways, and the choice matters on a device with no comfortable keyboard.

HTTP Basic works everywhere and uses your normal username and password — if the account has one.

A token is the better option for e-readers. Generate one on Settings; it is twelve characters from an alphabet with no I, L, O, 0 or 1 in it, precisely because it gets typed on a device with arrow keys and an on-screen grid.

https://comics.example.com/opds?token=XXXXXXXXXXXX

Revoking it on Settings cuts off every device using it at once.

An account that signs in through Google, Apple or Facebook

For these the token is not an alternative, it is the only route. Such an account has no password for a reader to send, and a reader cannot perform an OAuth sign-in on its behalf — that needs a browser, and OPDS has nowhere to put one.

Issue a token from the account page and use it exactly as above. Verified 2026-09-22 with a Google Workspace account and Panels on iOS.

Basic auth from such an account is refused with "Incorrect username or password", which is unhelpful on purpose: saying "this account has no password" to an unauthenticated caller would confirm the account exists. The account page says it instead, because it knows who is asking.

Point readers at https:// only

OPDS clients send your credentials with every request. Over plain HTTP that is your password, or a token that grants the same access, in clear text on every page turn.

Token guesses are throttled under the same machinery as failed logins, which is why LONGBOX_TRUSTED_PROXIES has to be right — a throttle keyed on a forgeable address is decorative.

Which clients work

The rule this project set itself is that the documentation claims no protocol version until a real client has opened it. Two have, against a live deployment, and what follows is what they actually did rather than what the specifications promise.

Client Platform Verified
Panels iOS, iPadOS, macOS OPDS 1.2: browsing, covers, downloads, page streaming, resume, token sign-in
Thorium Windows, macOS, Linux OPDS 2.0: browsing, facets, covers, metadata, acquisition

Details, dates and method are in OPDS clients. Both were confirmed by watching the server's access log, not by trusting a user interface — the difference between streaming and downloading is invisible from the outside.

So: OPDS 1.2 and OPDS 2.0 both work with real readers. Page streaming and resume work. A CBZ, a CBR and a PDF all read over the network, and the CBR is the interesting one — Panels almost certainly cannot open a .cbr itself, but streamed page by page it receives ordinary JPEGs, so the container stops mattering.

What is not claimed

Divina manifests. They validate against the published Readium schema and every page they list resolves, but no reader has opened one. Thorium is the obvious candidate and structurally cannot: it has no image-only rendering engine, and it is built for packaged publications rather than manifests streamed from a remote server. Both confirmed by its maintainer. Until a client opens one, this stays unclaimed.

The empty-feed deviation, still open. In a feed with no results the OPDS 2.0 prose requires a key that the published schema's minItems: 1 cannot accept with zero results. The empty array ships; omitting the key would break the prose, and inventing a navigation entry would answer a search for nothing with something. No client has been shown a genuinely empty result yet.

Two things worth knowing before you rely on them

Reading position travels one way. Longbox's position reaches a client through pse:lastRead; the client's never comes back. OPDS-PSE has no write-back and no ratified OPDS standard covers it — a synchronisation service was proposed in 2019 and is still a discussion. Panels does sync, with Komga and Kavita, through each server's proprietary API rather than over OPDS. Read on one device at a time and nothing is lost.

A cover is not decoration. A comic whose cover could not be extracted carries no image links in the feed, and at least one client hides such entries entirely rather than showing them plain. If a comic is missing from a reader but present in the web UI, check whether it has a cover first.

Clients decode the response, not the declared type. OPDS-PSE permits only image/jpeg, image/gif and image/png on a stream link, so a WebP or AVIF comic is advertised as JPEG and served as itself. Panels read all of them without complaint, which is why Longbox does not transcode pages.

Checking a deployed feed

tests/live_check.py walks a deployed server's feed the way a client does. It is hand-run and not part of the suites, because it is one of only two things under tests/ that touch the network.

python tests/live_check.py https://comics.example.com -u reader
python tests/live_check.py https://comics.example.com --token XXXXXXXXXXXX

The suites cannot do this job. A TestClient talks to the app object and skips TLS, the proxy and LONGBOX_PUBLIC_URL entirely — a feed whose links all point at http://127.0.0.1:8080 validates perfectly and is useless on a phone.