Architecture
Current system components, data flows, and deployment surfaces
Alex is a self-hosted PDF and EPUB library with web, Docker, and Electron delivery options. The source repository is a pnpm workspace containing the application, a shared UI package, and the optional Cloudflare relay.
System overview
Rendering diagram...
Workspace packages
| Path | Responsibility |
|---|---|
| Repository root | Next.js app, Electron shell, scripts, tests, and packaging |
packages/ui (@alex/ui) | Shared Radix-based components, style tokens, utilities, and stories |
watcher-rs | Rust ingestion engine, SQLite bridge, S3 streamer, and tunnel client |
alex-relay (@alex/relay) | Cloudflare Worker, Durable Object, protocol codec, and Worker tests |
Next.js application
The App Router serves the authenticated library, collections, readers, admin pages, public shared collections, and JSON/streaming APIs.
The two readers are client-only dynamic imports:
- PDF — PDF.js through
react-pdf; continuous page scroll, page tracking, zoom, fit, and document-wide search - EPUB — epub.js through
react-reader; continuous single-column scroll, table of contents, chapter navigation, responsive app theming, and saved font size
Tailwind CSS v4 styles application UI. Reusable primitives and CSS tokens live in @alex/ui; app-specific readers and library components remain under src/components.
Authentication
Alex supports browser and Electron requests on the same server:
- Browser users sign in with email/password through NextAuth v5. Passwords are bcrypt hashes and sessions are 30-day JWTs.
- Middleware uses a lightweight NextAuth configuration to decode the same JWT and enforce protected/admin routes.
- Electron generates a random token for each app process and injects
x-alex-desktop-authonly into requests to127.0.0.1:3210. Those requests use a synthetic admin session. - Remote requests arriving through the desktop relay do not have the Electron header, so normal Alex login still applies.
- Shared collection pages and APIs bypass login, then validate their UUID token and book membership.
Secure cookie prefixes are selected from NEXTAUTH_URL. Forwarded host/protocol headers keep redirects and login cookies correct behind the relay.
SQLite and the Rust bridge
The web process does not load a native Node SQLite module. src/lib/db/rust.ts starts a short-lived watcher-rs db process for each query or mutation, sending SQL as JSON and receiving JSON rows. The ingestion watcher uses rusqlite directly against the same database.
Connections use WAL mode, foreign keys, and a five-second busy timeout. SQL migrations live under src/lib/db/migrations; scripts/db-push.js creates the initial schema, repairs the unique book indexes for older databases, and adds S3 source columns when needed.
Ingestion
watcher-rs auto-selects one mode:
- Local when
S3_BUCKETis absent: recursively scan/watchLIBRARY_PATH, wait for writes to become stable, then handle additions, changes, and removals. - S3 when
S3_BUCKETis present: list the configured bucket/prefix on startup and on the polling interval, then diff keys and ETags against S3-backed book rows.
For PDF and EPUB files, the pipeline computes a SHA-256 hash, extracts metadata, writes a JPEG cover, updates SQLite, and changes settings.library_version. The authenticated SSE endpoint polls that value every two seconds so clients can refresh after ingestion changes.
Cover generation uses:
- PDF page 1 rendered at 150 DPI with PDFium
- Embedded EPUB cover art re-encoded as JPEG
- A synthetic 400×600 JPEG fallback with title and author
Book file serving
Private and shared reader routes use src/lib/files/serve-book-file.ts:
- Local books stream from disk with HTTP Range support.
- S3 books stream through the Rust
s3-streamcommand with Range passthrough.
Files stay in object storage in S3 mode. Alex stores metadata and covers locally, and browsers do not need bucket credentials or CORS access.
Electron desktop
In a packaged desktop app, Electron manages:
- A standalone Next.js server on
127.0.0.1:3210 - The local or S3 ingestion watcher after onboarding
- An optional
watcher-rs tunnelprocess when Public Access is enabled
The app stores a small config.json in Electron's user-data directory. It contains the library/S3 selection, generated NextAuth secret, window state, and relay settings. The SQLite database and cover directory also live under user data. This is a custom JSON store, not the electron-store package.
Public-access relay
alex-relay is a TypeScript Cloudflare Worker backed by hibernatable Durable Objects. The Worker routes each public hostname and tunnel WebSocket to the object named for that subdomain. One desktop socket registers with the object; concurrent browser requests are assigned IDs and multiplexed over its binary WebSocket.
The Rust client forwards each request to the local Next.js server, adds forwarded host/protocol headers, and streams the response in 64 KiB chunks. The Worker caps request bodies at 10 MiB and uses a 120-second inactivity timeout by default.
The relay removes the need for port forwarding, but it is not an authentication bypass. Normal library pages still require Alex credentials. Public shared-collection links are the only unauthenticated reader surface.
Deployment surfaces
| Surface | Current state |
|---|---|
| Local development | Next.js dev server plus watcher-rs in another terminal |
| Docker | Node 22 standalone server and prebuilt Rust binary; amd64 and arm64 images |
| macOS desktop | Signed/notarized Apple silicon DMG and ZIP |
| Linux desktop | x64/arm64 AppImage and deb packaging |
| Windows desktop | NSIS x64 packaging is configured, but the current release workflow does not publish it |
| Relay | Cloudflare Worker + SQLite-backed Durable Objects, deployed with Wrangler |
Technology stack
| Layer | Technology |
|---|---|
| Application | Next.js 16, React 19, TypeScript |
| UI | @alex/ui, Radix UI, Tailwind CSS v4, Storybook |
| Auth | NextAuth v5 credentials/JWT and Electron header auth |
| Database | SQLite through Rust rusqlite commands |
| Ingestion | Rust notify or S3 polling |
| Readers | PDF.js / react-pdf; epub.js / react-reader |
| Desktop | Electron 34, custom JSON settings store, system tray |
| Relay | Cloudflare Workers, Durable Objects, WebSockets |
| Tests | Jest, Rust tests/coverage, Playwright web/Electron, Chromatic |