BookBoss
Take Control Of Your Digital Library
BookBoss is a self-hosted application for managing your e-book collection. It provides a web-based interface for organising, browsing, and syncing your library across devices — backed by PostgreSQL, MySQL, MariaDB, or SQLite.
For Users
New to BookBoss? Start here:
- Installation — set up BookBoss on your server or machine
- Getting Started — first run, admin account setup
- Database Configuration — choose and configure your database backend
- Managing Your Library — importing books, browsing, editing metadata
- Shelves & Reading State — organise books into shelves, track reading progress
- Libraries — virtual collections for multi-user installs
- OPDS Catalog — browse your library from any OPDS-compatible reader app
- Kobo Device Sync — sync books and reading progress with Kobo e-readers
- Configuration Reference — all available configuration options
For Contributors
Looking to contribute or run from source?
- Architecture — hexagonal design, crate layout, domain modules
- Development Setup — tools, environment, first build
- Commands — all
justcommands - Conventions — commits, error handling, testing, secrets
- Frontend (Dioxus) — server functions, routing, auth, SSE events
- Database Internals — SeaORM, migrations, entity generation
Installation
Docker Compose (recommended)
The easiest way to run BookBoss is with Docker Compose. Pre-built compose files are provided
in the deploy/ directory for each
supported database backend.
Quick Start
-
Create directories for your library and bookdrop:
mkdir -p Library/Books Library/Bookdrop -
Download the compose file for your preferred database:
Backend File Notes PostgreSQL (recommended) docker-compose-postgres.yamlBest performance, recommended for production MySQL / MariaDB docker-compose-mysql.yamlAlternative relational backend SQLite docker-compose-sqlite.yamlSimplest setup, single container, no separate database # Example: PostgreSQL curl -O https://raw.githubusercontent.com/szinn/BookBoss/main/deploy/docker-compose-postgres.yaml -
Edit the compose file and change
BOOKBOSS__ENCRYPTION_SECRETto a random string. UpdateBOOKBOSS__FRONTEND__BASE_URLif you are accessing BookBoss from a different hostname or behind a reverse proxy. -
Start BookBoss:
docker compose -f docker-compose-postgres.yaml up -d -
Open http://localhost:8080 and create your admin account.
What’s Next
- Getting Started — first-run admin setup and orientation
- Database Configuration — details on each database backend
- Configuration Reference — all available environment variables
Requirements
BookBoss needs:
- A supported database — SQLite, PostgreSQL, MySQL, or MariaDB
- A filesystem directory for the book library (
BOOKBOSS__LIBRARY__LIBRARY_PATH) - A filesystem directory for the bookdrop inbox (
BOOKBOSS__IMPORT__BOOKDROP_PATH)
Build from Source
See Development Setup for instructions on building and running BookBoss from source.
Getting Started
First Run
On first launch, BookBoss detects that no admin account exists and prompts you to create one. You will need to provide:
- A username
- An email address
- A password
This account becomes the administrator of your BookBoss instance.
What to Do Next
- Add books — drop EPUB files into the bookdrop directory and review incoming imports
- Create shelves — organise your library with manual and smart shelves
- Set up libraries — in a multi-user install, create libraries to give each user a curated view of the collection
- Set up OPDS — generate an OPDS password to browse your library from reader apps
- Connect a Kobo — register your Kobo e-reader to sync books and reading progress
- Invite users — add additional user accounts from the Settings page
Additional Configuration
- Database Configuration — choose and configure your database backend
- Configuration Reference — all available settings
Database Configuration
BookBoss supports four database backends. Choose the one that fits your deployment:
| Database | Best for |
|---|---|
| SQLite | Single-user, low maintenance, simple setups |
| PostgreSQL | Multi-user, production deployments |
| MariaDB | Existing MariaDB infrastructure |
| MySQL | Existing MySQL infrastructure |
SQLite
SQLite requires no separate server — the database is a single file on disk.
Set BOOKBOSS__DATABASE__DATABASE_URL to a file path:
sqlite:///path/to/bookboss.db
Or use a relative path:
sqlite://./bookboss.db
Tip: SQLite is the simplest option for personal use. No additional software required.
PostgreSQL
PostgreSQL is recommended for multi-user or production deployments.
Prerequisites
A running PostgreSQL instance is required. You can run one with Docker:
docker run -d \
--name bookboss-postgres \
-e POSTGRES_USER=bookboss \
-e POSTGRES_PASSWORD=yourpassword \
-e POSTGRES_DB=bookboss \
-p 5432:5432 \
postgres:16
Configuration
BOOKBOSS__DATABASE__DATABASE_URL=postgres://user:password@host:5432/database
MySQL / MariaDB
Prerequisites
A running MySQL or MariaDB instance is required. You can run one with Docker:
docker run -d \
--name bookboss-mysql \
-e MYSQL_USER=bookboss \
-e MYSQL_PASSWORD=yourpassword \
-e MYSQL_DATABASE=bookboss \
-e MYSQL_ROOT_PASSWORD=rootpassword \
-p 3306:3306 \
mysql:8
Configuration
Both MySQL and MariaDB use the same connection string format:
BOOKBOSS__DATABASE__DATABASE_URL=mysql://user:password@host:3306/database
Migrations
BookBoss applies database migrations automatically on startup. No manual steps are required.
Managing Your Library
Adding Books
BookBoss acquires books through a bookdrop directory. The pipeline runs automatically in the background.
Workflow
- Drop a file into the directory configured as
BOOKBOSS__IMPORT__BOOKDROP_PATH - BookBoss picks it up — the scanner runs on a configurable interval (default: 60 seconds) and hashes each new file to avoid duplicates. You can also trigger a scan manually from the UI.
- Metadata is extracted — for EPUB files, embedded metadata is read from the OPF inside the archive; other formats fall through to provider lookup
- Provider enrichment — BookBoss queries external metadata providers (Hardcover, Open Library, Google Books) in parallel, using title+author similarity scoring to select the best match. Cover art is fetched from the most confident provider.
- Review queue — the book lands in the Incoming section of the library (requires the Approve Imports capability)
Reviewing and Approving
Navigate to Library > Incoming to see books awaiting review.
Each review page shows three columns: the field name, the current extracted value (editable), and the value fetched from the metadata provider. Use the copy buttons to pull individual fields from the provider into the current value.
- Fetch provider data — re-queries the provider using the current identifiers in the form
- Libraries — assign the book to one or more libraries at approval time (requires custom libraries to exist)
- Approve — commits the edited metadata, moves the book to your library, and sets its status to Available. The book is always added to All Books; any libraries ticked in the Libraries field are added as well.
- Reject — discards the import
- Cancel — returns to the Incoming list without changes
File Storage
Approved books are stored under BOOKBOSS__LIBRARY__LIBRARY_PATH with the layout:
{library_path}/
└── BK_<token>/
├── <author>-<title>.epub # the book file (slug derived from author + title)
├── cover.jpg # cover image
└── metadata.opf # OPF sidecar with all metadata
Duplicate Detection
Files are SHA-256 hashed before ingestion. If a file with the same hash already exists in the library, the import is skipped automatically.
Browsing Your Library
Book Grid
The main Library view shows your approved books as a grid of cover thumbnails.
The grid is scoped to the active library. Use the library picker in the NavBar to switch libraries, or click the Home button to return to your default library. Selecting All Books shows every book in the catalogue.
- Sort books by date added, title, or author using the sort controls
- Search using the search bar in the navigation bar — type to filter the displayed books. Prefix terms with
author:,title:,series:,genre:, ortag:for targeted filtering. Search is also scoped to the active library. - Filter by shelf using the shelf pills at the top of the screen
- Add to library — drag a book cover onto the Home icon in the NavBar to add it to your default library
Book Detail Page
Click any book to open its detail page. From there you can:
- View full metadata — title, authors, series, description, published year, page count, language, and identifiers (ISBN, ASIN, Hardcover, etc.)
- Download the book file — format badges (e.g.
EPUB 2.3 MB) are download links - Edit metadata — opens the edit page (requires the Edit Book capability)
- Delete the book (requires the Delete Book capability)
Multi-Select & Bulk Operations
Select multiple books from the grid to perform bulk operations:
- Set reading status — change the reading status for all selected books at once
- Edit metadata — bulk edit a subset of fields (genres, tags, libraries, etc.) across selected books
- Add to Library — add all selected books to a library (admin / Edit Book capability; a dropdown picker lists non-system libraries). Only shown when custom libraries exist.
Keyboard shortcuts are available for common actions.
Deleting Books & Trash
When you delete a book (from the book detail page or via multi-select), BookBoss removes it from the database and deletes its library files. Before deletion, the enriched book file (with metadata and cover art baked in) is automatically copied to a Trash directory:
{library_path}/Trash/
└── author-title.epub # enriched copy, ready for re-import
This acts as a filesystem safety net — if you change your mind, recovering the book is as simple as copying the file from Trash back into your Bookdrop directory. The scanner will pick it up, extract the embedded metadata, and run it through the normal import pipeline.
A few details:
- Only the enriched file is copied to Trash (the version with metadata and cover embedded). If no enriched file exists, nothing is placed in Trash.
- If a file with the same name already exists in Trash, it is overwritten with the newer version.
- Rejected imports do not go to Trash. The original file was moved out of Bookdrop during ingestion and rejecting the import deletes it permanently.
- There is no automatic retention policy. Clean up the Trash directory manually when you are ready to free the disk space.
Editing Metadata
Click Edit on any book’s detail page to open the metadata editor. You can update:
- Title, description, language, page count, published year
- Authors (with roles: Author, Editor, Translator, Illustrator)
- Series name and number
- Cover image
- Genres, tags, publishers
- Identifiers (ISBN-13, ISBN-10, ASIN, etc.)
- Libraries — which libraries the book belongs to (only shown when custom libraries exist)
Changes are saved to the database and to the OPF sidecar file on disk.
Downloading Books
Format download links appear on each book’s detail page under the Formats section. Each badge shows the format name and file size. Clicking downloads an enriched copy of the book with up-to-date metadata and cover art embedded.
Shelves & Reading State
Shelves
Shelves are named collections you create to organise your books. Each shelf belongs to a library — the shelf pills shown at the top of the book grid are those that belong to the currently active library. There are two types of shelf:
Manual Shelves
Manual shelves contain exactly the books you add to them.
Creating a shelf: Open Settings > Shelves or use the sidebar to create a new shelf.
Adding books: Drag a book’s cover from the grid onto a shelf pill at the top of the screen.
Removing books: Open the shelf, then remove individual books from the shelf view.
Smart Shelves
Smart shelves are defined by filter rules and update automatically. For example, you can create a smart shelf that contains all books with reading status “Reading”, or all books by a specific author.
Smart shelves recalculate their contents whenever books or reading state change — no manual maintenance needed.
Available filter rules include:
| Rule | Description |
|---|---|
| Reading status | Match by read/reading/unread/etc. |
| Author | Match books by a specific author |
| Series | Match books in a specific series |
| Genre / Tag | Match books with a specific genre or tag |
| Library | Match books that belong to a specific library (admin only) |
The Library rule is only visible in the filter builder for admin accounts. It lets you build cross-library smart collections, such as a shelf that spans books from two different personal libraries.
Reading State
Each user tracks their own reading state per book. Reading status options are:
| Status | Meaning |
|---|---|
| Unread | Default — book has not been read |
| Reading | Currently reading |
| Paused | Reading paused |
| Rereading | Reading again |
| Read | Finished reading |
| Abandoned | Stopped reading, not planning to finish |
Setting Reading Status
You can set reading status in several ways:
- Book detail page — change the status for a single book
- Bulk select — select multiple books in the grid and set status for all at once
- Kobo sync — reading progress syncs automatically from Kobo devices
Reading state is per-user — each user in a multi-user setup has independent tracking.
Libraries
Libraries are virtual collections that let you slice the full book catalogue into curated views. Each user sees only the books in their active library. A multi-user household might have a shared family library alongside personal ones; a single-user install can ignore libraries entirely and use the built-in All Books library.
How Libraries Work
Every approved book is automatically added to the All Books system library. This library always exists, contains every book in the catalogue, and cannot be deleted or renamed.
Beyond All Books, an administrator can create:
- Shared libraries — admin-created collections assigned to any number of users. Useful for genre or topic collections shared across the household.
- Personal libraries — user-owned collections seeded from the user’s existing shelves and reading state. Each user can have at most one personal library.
Users only see books that belong to their active library. The active library is switched from the NavBar picker, and the home button always returns to the user’s default library.
For Administrators
Settings → Libraries
Navigate to Settings → Libraries to manage the library catalogue.
The panel lists every library with its user count and book count:
- All Books — displays a grey system badge; the delete button is absent. You cannot delete or rename this library.
- Custom libraries — have a delete button (trash icon). Deleting a library re-parents all shelves that belonged to it back to All Books, and resets any user whose default was that library back to All Books. The books themselves are not deleted.
Creating a library:
- Click Create Library.
- Enter a unique name and confirm.
The new library starts empty. Assign users to it and add books via bulk operations or the edit-metadata form.
Adding books to a library:
- From the book grid, select books and use Add to Library in the selection bar.
- From the bulk edit modal, use the Libraries checkboxes.
- From the edit-metadata or incoming review form, use the Libraries field.
- Drag a book cover onto the Home icon in the NavBar to add it to your default library.
Settings → Users — Library Assignment
When you create or edit a user, the modal includes a library management section:
Library checkboxes — tick each library the user should have access to. Users can only browse libraries they are assigned to. All Books is always available and cannot be unassigned.
Default library — a picker (shown once the user is assigned to two or more libraries) sets which library loads when the user first logs in or clicks the Home button.
Personal library — a checkbox (shown only when the user does not yet have one) creates a personal library for the user:
- Enter a name or accept the auto-filled suggestion.
- On save, BookBoss creates the library, assigns the user to it, re-parents their existing shelves into it, and seeds it with books they already have a shelf or reading-state relationship with.
- The personal library is set as the user’s default.
Renaming a personal library — when a user already has a personal library, an editable name field replaces the creation checkbox. The rename is applied when you save the form.
Deleting a personal library — click the × button next to the name field. An inline confirmation appears. Confirm to mark it for deletion; an Undo link reverses the choice before you save. The deletion is applied on save: shelves re-parent to All Books and the user’s default resets to All Books.
For Users
Switching Libraries
If you are assigned to two or more libraries, a library picker appears in the NavBar between the search bar and the user menu. Select a library from the dropdown to scope the book grid and search to that collection.
The Home button (house icon) is always visible. Clicking it switches back to your default library.
Setting Your Default Library
Go to your Profile page. If you have two or more assigned libraries a Default Library picker is shown. The library you choose here is the one that loads on login and when you click the Home button.
Adding Books to Your Library
Drag any book cover from the grid onto the Home icon in the NavBar. The icon briefly turns green to confirm the drop. The book is added to your current default library.
Libraries and Shelves
Each shelf belongs to a library. When you are browsing a library, the shelf pills at the top of the screen show only the shelves that belong to that library. Shelves created while a personal library is active are automatically placed in that library.
If your personal library is ever deleted by an administrator, your shelves are moved back to All Books rather than being deleted.
Library Filter Rule in Smart Shelves
When building a smart shelf filter (admin accounts only), a Library rule is available. This matches books that belong to a specific library, and can be combined with other rules to build cross-library smart collections.
OPDS Catalog
BookBoss includes a built-in OPDS 1.x catalog server, allowing you to browse and download books from any OPDS-compatible reader application.
Setup
-
Go to your Profile page
-
Your OPDS password is auto-generated — copy it, or click Regenerate to create a new one
-
In your reader app, add a new OPDS catalog with:
- URL:
http://<your-bookboss-host>:<port>/opds/ - Username: your BookBoss username
- Password: your OPDS password (not your login password)
If your reader app does not have separate username/password fields, you can embed the credentials in the URL:
http://username:password@<your-bookboss-host>:<port>/opds/ - URL:
Note: OPDS uses a separate password from your regular BookBoss login. This is by design — OPDS uses HTTP Basic Auth, which transmits credentials with every request.
Compatible Apps
Any OPDS 1.x compatible reader should work, including:
- KOReader
- Librera Reader
- Moon+ Reader
- Aldiko
- FBReader
- Calibre
Available Feeds
The OPDS catalog provides the following navigation:
| Feed | URL | Description |
|---|---|---|
| Root catalog | /opds/ | Entry point; shows “Libraries” nav entry for users with 2+ libraries |
| Default library | /opds/all | Books in the user’s default library (not the full catalogue) |
| Libraries | /opds/libraries | Navigation feed listing the user’s assigned libraries |
| Library | /opds/libraries/{token} | Books in a specific library |
| Search | /opds/search?q=... | Full-text book search |
| Shelves | /opds/shelves | Browse by shelf |
| Authors | /opds/authors | Browse by author |
| Series | /opds/series | Browse by series |
Each book entry includes download links for all available formats and cover images.
Note:
/opds/allis scoped to the user’s default library. To browse a different library, navigate via the Libraries feed (/opds/libraries) and select a specific library.
Capabilities
OPDS access requires the OPDS Access capability. Administrators can grant this to users from the user management settings.
Kobo Device Sync
BookBoss can sync books and reading progress directly to Kobo e-readers. Your Kobo connects to BookBoss as if it were the Kobo store, receiving books and syncing reading state.
Registering a Device
- Go to your Profile page
- Click Add Device and give it a name
- Copy the sync URL — you will need to configure your Kobo to use this URL
Configuring Your Kobo
The sync URL must be set in your Kobo’s configuration database. The exact setup depends on your Kobo model and firmware version. The sync URL replaces the default Kobo store API endpoint so that the device communicates with BookBoss instead.
How Sync Works
Each registered Kobo device is paired with a companion smart shelf. To sync books to a device:
- Edit the companion shelf’s filter rules to select the books you want to sync (e.g. by reading status, author, genre, or tag)
- The next time your Kobo syncs, it will download the matching books
Sync is incremental — only new or changed books are sent each time the device connects.
Supported Formats
- EPUB — served as-is
- KEPUB — BookBoss converts EPUBs to KEPUB format automatically for optimal Kobo rendering
Reading State Sync
Reading progress syncs bidirectionally between your Kobo and BookBoss:
- Position — your current reading position in the book
- Progress — percentage complete
- Time spent — reading time tracked by the Kobo
When you read on your Kobo, the progress appears in BookBoss. Reading state changes in BookBoss are reflected on the device at next sync.
Managing Devices
From your Profile page you can:
- Copy sync URL — copy the device sync URL to your clipboard
- Reset sync — force a full re-sync (the device will re-download all books)
- Configure on-removal action — choose what happens when a book is removed from the device
Troubleshooting
If your Kobo is not syncing:
- Ensure the Kobo is connected to the same network as your BookBoss instance
- Verify the sync URL is correctly configured on the device
- Check that books have been added to the device’s companion shelf
- Try resetting the sync state from the Profile page
Configuration Reference
Configuration is loaded from environment variables with the prefix BOOKBOSS and __ as the
separator (e.g. BOOKBOSS__DATABASE__DATABASE_URL).
Secrets are stored in an encrypted config.sops.env file managed by sops. Run just config to edit it.
Database
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__DATABASE__DATABASE_URL | Database connection string (required) | — |
See Database Configuration for connection string formats and examples.
Encryption
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__ENCRYPTION_SECRET | Encryption key for sensitive data (OPDS passwords) (required) | — |
Frontend
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__FRONTEND__LISTEN_IP | IP address the web server listens on | 0.0.0.0 |
BOOKBOSS__FRONTEND__LISTEN_PORT | Port the web server listens on | 8080 |
BOOKBOSS__FRONTEND__BASE_URL | Public-facing base URL (used for Kobo sync URLs) | http://0.0.0.0:8080 |
Library
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__LIBRARY__LIBRARY_PATH | Path where approved book files are stored (required) | — |
Import
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__IMPORT__BOOKDROP_PATH | Directory to watch for new e-book files (required) | — |
BOOKBOSS__IMPORT__SCAN_INTERVAL_SECS | How often (seconds) to scan the bookdrop directory | 60 |
BOOKBOSS__IMPORT__WORKER_POLL_INTERVAL_SECS | How often (seconds) the import worker polls for jobs | 5 |
Metadata Providers
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__METADATA__HARDCOVER_API_TOKEN | API token for Hardcover (primary metadata provider) | — |
BOOKBOSS__METADATA__GOOGLEBOOKS_API_TOKEN | API token for Google Books | — |
Open Library does not require an API token.
Providers are queried in parallel. The best match is selected by title+author similarity scoring.
API (gRPC)
| Variable | Description | Default |
|---|---|---|
BOOKBOSS__API__GRPC_LISTEN_IP | IP address the gRPC server listens on | 0.0.0.0 |
BOOKBOSS__API__GRPC_LISTEN_PORT | Port the gRPC server listens on | 8081 |
Database Admin (just commands)
These variables are used by just create-database and just database, not by BookBoss itself:
| Variable | Used by |
|---|---|
PGUSER | just create-database, just database |
PGPASSWORD | just create-database, just database |
PGDATABASE | just create-database, just database |
PGADMINUSER | just create-database |
PGADMINPASSWORD | just create-database |
Architecture
BookBoss follows hexagonal (ports & adapters) architecture. Dependencies point inward toward the core domain. The core crate never depends on any outer crate.
Crate Layout
crates/
├── api/ # Adapter: gRPC interface, calls into core ports
├── core/ # Domain layer: business logic, models, port traits
├── database/ # Adapter: persistence (SeaORM — Postgres, MySQL, MariaDB, SQLite)
├── formats/ # Adapter: e-book file formats (EPUB, OPF, KEPUB conversion)
├── frontend/ # Adapter: Dioxus web UI, OPDS catalog server, Kobo sync protocol
├── import/ # Adapter: library scanner + import job handler
├── metadata/ # Adapter: external metadata providers (Hardcover, OpenLibrary, GoogleBooks)
├── storage/ # Adapter: local filesystem library store
├── utils/ # Shared utilities (hashing, string similarity, token generation)
├── bookboss/ # Entry point: wires adapters to ports, loads configuration
└── integration-tests/
Core Crate
The core crate uses domain-based modules. Each domain groups its model, repository trait (port), and service:
crates/core/src/
├── lib.rs # CoreServices composition root, create_services()
├── error.rs # Error, ErrorKind, RepositoryError
├── types.rs # Shared newtypes (Email, Capability, etc.)
├── repository.rs # Repository, Transaction traits; RepositoryService; transaction macros
├── test_support.rs # Mock implementations (behind "test-support" feature)
├── auth/ # Session auth: Session, AuthService, SessionRepository
├── book/ # Books, authors, series, publishers, genres, tags, files
├── conversion/ # ConversionService port trait (EPUB enrichment, KEPUB conversion)
├── device/ # Device sync: Device, DeviceBook, DeviceSyncLog
├── event/ # EventService: broadcast channel for real-time UI updates (SSE)
├── filter/ # BookFilter, FilterCondition, operators (for smart shelves)
├── import/ # Acquisition pipeline: ImportJob, ImportJobService
├── jobs/ # Job queue: Job, JobRepository, JobWorker, JobService, JobHandler
├── library/ # LibraryService (delete_book, library_stats), LibraryRepository
├── opds/ # OpdsService port (OPDS password management)
├── pipeline/ # Port traits: MetadataExtractor, MetadataProvider; PipelineService
├── reading/ # Per-user reading state: UserBookMetadata, ReadStatus
├── shelf/ # Shelves (manual + smart): Shelf, ShelfFilter, ShelfService
├── storage/ # FileStoreService port trait + BookSidecar struct
└── user/ # Users and settings: User, UserService, UserSettingService
Each domain module typically contains:
mod.rs— re-exportsmodel.rs(ormodel/) — domain types (Foo,NewFoo,FooId,FooToken)repository.rs(orrepository/) —FooRepositorytrait (port)service.rs—FooServicetrait +FooServiceImpl
Metadata Providers
The metadata crate implements the MetadataProvider port from core. Providers are queried in parallel with title+author similarity scoring to select the best match:
- Hardcover — primary provider (GraphQL API); returns metadata, cover, ratings, genres
- Open Library — ISBN-based and title search fallback; returns metadata and cover
- Google Books — additional fallback; returns metadata and cover
Each provider implements MetadataProvider::enrich(extracted) -> Option<ProviderBook>.
Import Pipeline
The import subsystem (crates/import/) owns the library scanner:
- LibraryScanner — polls
BOOKBOSS__IMPORT__BOOKDROP_PATHon a timer (or via manual trigger), hashes new files, and enqueuesImportJobrecords
The import worker runs via the core job system (CoreSubsystem/JobWorker):
- Processes
ImportJobrecords through thePipelineService: extract metadata → enrich from providers → create book record → write sidecar → queue for review
Job System
The core job system provides a generic background task framework:
- JobService — manages job enqueueing, handler registration, and dispatch
- JobWorker — polls for pending jobs and dispatches to handlers via
JobService - JobHandler — trait implemented by each handler (import pipeline, EPUB enrichment, KEPUB conversion)
Jobs are persisted in the database with status tracking (Pending, Processing, Completed, Failed). Recovery runs on startup to retry stalled jobs.
Event System
The EventService broadcasts real-time events via a tokio broadcast channel:
IncomingChanged— fired when imports reach NeedsReview, or are approved/rejectedJobsChanged— fired when background jobs are queued, completed, or failed
The frontend exposes these as Server-Sent Events (SSE) at GET /api/v1/events for live UI updates.
Subsystem Pattern
Each crate that owns background work exposes an XxxSubsystem struct and create_xxx_subsystem() factory. Subsystems are composed in bookboss/main.rs via tokio-graceful-shutdown:
#![allow(unused)]
fn main() {
Toplevel::new()
.start(SubsystemBuilder::new("api", api_subsystem.run()))
.start(SubsystemBuilder::new("import", import_subsystem.run()))
.start(SubsystemBuilder::new("core", core_subsystem.run()))
...
}
Current subsystems: ApiSubsystem (gRPC), CoreSubsystem (job worker), ImportSubsystem (library scanner).
Adding a New Domain
- Create a directory under
crates/core/src/(e.g.order/) - Add
mod.rs,model.rs,repository.rs,service.rs - Re-export from
mod.rs - Register the module in
lib.rs - Wire the new service into
CoreServices
Import Conventions
Use flat re-exports from domain modules:
#![allow(unused)]
fn main() {
use crate::user::{User, UserService, UserId}; // not user::model::User
use crate::session::{Session, NewSession};
use crate::repository::{Repository, Transaction};
use crate::types::{Email, Age};
}
Cross-domain references are allowed (e.g. use crate::user::UserId in a shelf model for foreign keys). Keep references one-directional where possible.
Development Setup
Prerequisites
- Rust (nightly toolchain for formatting and clippy;
rust-version = "1.85"edition 2024) - mise — tool version manager
- just — command runner
- direnv — environment setup
- sops — secrets encryption
- Node.js 24+ (managed by mise, used for Tailwind CSS)
One-time Setup
Install and update all required tools:
just install-tools
This installs the nightly toolchain, wasm32 target, cargo extensions, and any other project tools.
Secrets Configuration
just config
This opens the encrypted config.sops.env file for editing. See Configuration Reference for all available settings.
Building
just build
Running
just run
The application will be available at http://localhost:8080 by default.
Integration Tests
Integration tests require Docker via Colima:
colima start
just test
colima stop
See Commands for the full list of test commands.
Commands
All commands are run via just.
Development
| Command | Description |
|---|---|
just build | Build the project |
just run | Run the application (Dioxus dev server) |
just fmt | Format code (nightly rustfmt + Prettier) |
just clippy | Run Clippy lints (nightly) |
just clean | Clean the workspace |
just deps | Update Rust crate dependencies |
just tailwindcss | Regenerate Tailwind CSS |
just config | Edit encrypted configuration (sops) |
just install-tools | Install mise, nightly Rust, wasm32 target |
just bundle | Bundle web + server for release |
Testing
| Command | Description |
|---|---|
just component-tests | Component/unit tests only (nextest) |
just quick-test | Component + Postgres + SQLite integration tests |
just test | All tests (all database backends) |
just integration-tests | All integration tests (requires Colima) |
just postgres-integration-tests | Postgres integration tests |
just sqlite-integration-tests | SQLite integration tests |
just mysql-integration-tests | MySQL integration tests |
just mariadb-integration-tests | MariaDB integration tests |
just insta | Run insta snapshot tests |
just insta-review | Review insta snapshot deltas |
Database
| Command | Description |
|---|---|
just database | Database admin |
just create-database | Create the Postgres database and user |
Documentation
| Command | Description |
|---|---|
just docs-serve | Serve documentation locally (mdBook) |
just docs-build | Build documentation |
Release
| Command | Description |
|---|---|
just changelog | Generate changelog from git history |
just release VERSION | Create a release |
Conventions
Version Control
This project uses jujutsu (jj), not git directly.
Key commands:
| Command | Description |
|---|---|
jj commit | Commit current changes |
jj describe -m "…" | Update the working copy description |
jj new | Start a new change |
jj log | Show history |
jj status | Show working copy status |
Commit Messages
Follow Conventional Commits with crate-based scopes:
type(scope): description
Valid scopes: api, cli, core, database, frontend, import, metadata, formats, storage (match crate names).
Examples:
feat(core): add book domain with service and repository port
fix(database): handle null author field in entity mapping
refactor(frontend): simplify extension extraction
Use jj describe -m "..." to set the working copy description. Do not amend published commits.
Error Handling
| Crate | Approach |
|---|---|
core, api, database | thiserror for typed errors |
bookboss (binary entry point) | anyhow for ad-hoc errors |
Dependencies
All crate dependencies are defined in the root Cargo.toml under [workspace.dependencies].
Individual crates reference them with crate-name.workspace = true.
In root Cargo.toml:
- Version-only deps: inline format —
anyhow = "1.0.100" - Deps with features: section format:
[workspace.dependencies.uuid]
version = "1"
features = ["v4", "serde"]
Secrets
Secrets are encrypted with sops. Never commit plaintext secrets.
Testing
- Use
cargo-nextestas the test runner (just test) - Use
cargo-instafor snapshot tests when asserting against larger or structured output - Use regular assertions for simple value checks
- Tests live alongside source code in
#[cfg(test)]modules - Integration tests run against real database containers (Postgres, MySQL, MariaDB, SQLite) via testcontainers + Colima
End-of-Task Routine
Run these steps in order after completing each task:
just fmt— format codejust clippy— lint (run separately from fmt, not chained)just component-tests— verify tests passjj desc -m "type(scope): description\n\nbody"— update working copy description
Frontend (Dioxus)
The frontend is built with Dioxus 0.7 in fullstack mode — server-side rendering with client-side hydration, using axum as the server.
Warning: Dioxus 0.7 is a major API break from earlier versions.
cx,Scope, anduse_stateare gone. Only use 0.7 documentation and patterns.
Key Patterns
Server Functions
Use #[get] or #[post] to define server functions. The macro takes the endpoint path followed by
any axum extensions the function needs, declared as name: axum::Extension<Type>. These are
injected server-side and are not part of the function’s parameter list.
#![allow(unused)]
fn main() {
#[get("/api/v1/check_auth", auth_session: axum::Extension<AuthSession>)]
async fn check_auth() -> Result<bool, ServerFnError> {
Ok(auth_session.current_user.as_ref().map(|u| !u.username.is_empty()).unwrap_or(false))
}
}
Function parameters (the request arguments) are declared normally on the function:
#![allow(unused)]
fn main() {
#[post("/api/v1/login", core_services: axum::Extension<Arc<CoreServices>>, auth_session: axum::Extension<AuthSession>)]
async fn perform_login(username: String, password: String) -> Result<(), ServerFnError> {
// username and password come from the caller
// core_services and auth_session are injected by axum
}
}
Use #[tracing::instrument] to add tracing — always skip the injected extensions:
#![allow(unused)]
fn main() {
#[get("/api/v1/get_landing_state", core_services: axum::Extension<Arc<CoreServices>>, auth_session: axum::Extension<AuthSession>)]
#[tracing::instrument(level = "trace", skip(core_services, auth_session))]
async fn get_landing_state() -> Result<LandingState, ServerFnError> { ... }
}
The server-side imports (AuthSession, CoreServices, etc.) are gated behind the server feature:
#![allow(unused)]
fn main() {
#[cfg(feature = "server")]
use {crate::server::AuthSession, bb_core::CoreServices, std::sync::Arc};
}
Axum Handlers (non-server-fn routes)
Some endpoints need full axum handler control — for example, file downloads, image serving,
OPDS feeds, and the Kobo sync protocol. These live in crates/frontend/src/server/ and are
registered manually in server/mod.rs.
Follow the pattern in covers.rs / downloads.rs: check auth via auth_session.current_user,
return Response directly, use Body::from(data) with appropriate Content-Type and
Cache-Control headers.
Auth / Session
AuthSessionis stored in request extensions byAuthSessionLayer- Check
!user.username.is_empty()to determine if the user is authenticated (anonymous users have empty usernames) auth_session.login_user(user_id)logs in a user- Capability checks use
Auth::build([Method::POST], true).requires(...).validate(...)— do not use.permissions.contains()directly, as it misses transitive grants from Admin/SuperAdmin
Routing
Routes are defined as a Routable enum. LandingPage lives outside AppLayout (no NavBar):
#![allow(unused)]
fn main() {
#[derive(Routable, Clone, PartialEq)]
enum Route {
#[route("/")]
LandingPage {}, // no layout — appears before #[layout(...)]
#[layout(AppLayout)]
#[route("/library")]
BooksPage {},
#[route("/library/books/:token")]
BookDetailPage { token: String },
#[route("/library/books/:token/edit")]
EditMetadataPage { token: String },
#[route("/library/authors/:token")]
AuthorDetailPage { token: String },
#[route("/library/series/:token")]
SeriesDetailPage { token: String },
#[route("/library/incoming")]
IncomingPage {},
#[route("/library/incoming/:token")]
ReviewPage { token: String },
#[route("/shelves/:token")]
ShelfPage { token: String },
#[route("/settings")]
SettingsPage {},
#[route("/profile")]
ProfilePage {},
}
}
Navigate programmatically after a server fn succeeds:
#![allow(unused)]
fn main() {
let navigator = use_navigator();
navigator.push(Route::BooksPage {});
}
Hydration
Use use_server_future (not use_resource) for data that must be available on first render:
#![allow(unused)]
fn main() {
let data = use_server_future(fetch_data)?;
}
Browser-specific code (e.g. localStorage) must go inside use_effect, which runs only after hydration.
Server Modules
crates/frontend/src/server/
├── mod.rs # Server setup, router, middleware stack
├── auth_user.rs # AuthUser impl for axum-session-auth
├── session_pool.rs # BackendSessionPool (session store)
├── covers.rs # GET /api/v1/covers/{token} — serve cover images
├── downloads.rs # GET /api/v1/books/{token}/download/{format}
├── events.rs # GET /api/v1/events — SSE event stream
├── opds/ # OPDS 1.x catalog server (Atom XML feeds)
│ ├── mod.rs # Router, auth extractor
│ ├── feeds.rs # All OPDS feed endpoints
│ ├── xml.rs # Atom XML builder
│ └── ...
└── kobo/ # Kobo device sync protocol
├── mod.rs # Router, auth extractor
├── initialization.rs # Device init + store API proxy
├── library_sync.rs # Incremental library sync
├── metadata.rs # Per-book metadata
├── state.rs # Reading state GET/PUT
└── ...
Frontend Structure
crates/frontend/src/
├── lib.rs # Route enum, AppLayout, root component
├── settings.rs # FrontendConfig
├── error.rs # Error types
│
├── routes/
│ ├── landing_page.rs # Login, register admin
│ ├── books_page.rs # Library grid with search, sort, bulk ops
│ ├── book_detail_page.rs # Book detail + download + delete
│ ├── edit_metadata_page.rs # Metadata editor
│ ├── author_detail_page.rs # Author detail + books list
│ ├── series_detail_page.rs # Series detail + books list
│ ├── shelf_page.rs # Shelf contents view
│ ├── incoming_page.rs # Import review queue
│ ├── settings_page.rs # Settings, user management
│ ├── profile_page.rs # User profile, OPDS, Kobo devices
│ └── review_page/
│ ├── mod.rs # ReviewPage component
│ ├── editor.rs # Side-by-side metadata editor
│ ├── server.rs # Server functions
│ └── types.rs # ReviewBook, ReviewField, etc.
│
├── components/
│ ├── app_layout.rs # AppLayout wrapper (NavBar + outlet)
│ ├── nav_bar.rs # Top navigation bar with search
│ ├── book_grid.rs # Cover thumbnail grid (DnD, multi-select)
│ ├── shelf_bar.rs # Horizontal shelf pills (DnD drop targets)
│ ├── autocomplete_input.rs # Typeahead input for authors, series, etc.
│ ├── chip_input.rs # Tag/genre chip input
│ ├── login_form.rs # Login form
│ └── register_admin_form.rs # Admin registration form
│
└── server/ # (see Server Modules above)
SSE Events
BookBoss uses Server-Sent Events for real-time UI updates. The GET /api/v1/events endpoint
streams AppEvent variants:
IncomingChanged— new imports ready for review, or imports approved/rejectedJobsChanged— background job status changes
The frontend subscribes to this stream and updates relevant UI components automatically (e.g. the incoming badge count refreshes without a page reload).
Database Internals
BookBoss uses SeaORM for database access. PostgreSQL, MySQL, MariaDB, and SQLite are all supported.
Environment Variables
The following environment variables are used by database-related just commands:
| Variable | Used by |
|---|---|
PGUSER | just create-database, just database |
PGPASSWORD | just create-database, just database |
PGDATABASE | just create-database, just database |
PGADMINUSER | just create-database |
PGADMINPASSWORD | just create-database |
BOOKBOSS__DATABASE__DATABASE_URL | Migrations, entity generation |
Connection string format for BOOKBOSS__DATABASE__DATABASE_URL:
- PostgreSQL:
postgres://user:password@host:port/database - MySQL:
mysql://user:password@host:port/database - SQLite:
sqlite::/path/to/fileorsqlite::memory:
Warning: Secrets must be encrypted with
sops. Never commit plaintext credentials.
Migrations
Migrations run automatically on application startup. No manual steps are required.
Integration Tests
Integration tests for each backend run in Docker via Colima:
colima start
just postgres-integration-tests
just mysql-integration-tests
just mariadb-integration-tests
just sqlite-integration-tests
colima stop