Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

For Contributors

Looking to contribute or run from source?

Installation

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

  1. Create directories for your library and bookdrop:

    mkdir -p Library/Books Library/Bookdrop
    
  2. Download the compose file for your preferred database:

    BackendFileNotes
    PostgreSQL (recommended)docker-compose-postgres.yamlBest performance, recommended for production
    MySQL / MariaDBdocker-compose-mysql.yamlAlternative relational backend
    SQLitedocker-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
    
  3. Edit the compose file and change BOOKBOSS__ENCRYPTION_SECRET to a random string. Update BOOKBOSS__FRONTEND__BASE_URL if you are accessing BookBoss from a different hostname or behind a reverse proxy.

  4. Start BookBoss:

    docker compose -f docker-compose-postgres.yaml up -d
    
  5. Open http://localhost:8080 and create your admin account.

What’s Next

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

  1. Add books — drop EPUB files into the bookdrop directory and review incoming imports
  2. Create shelves — organise your library with manual and smart shelves
  3. Set up libraries — in a multi-user install, create libraries to give each user a curated view of the collection
  4. Set up OPDSgenerate an OPDS password to browse your library from reader apps
  5. Connect a Koboregister your Kobo e-reader to sync books and reading progress
  6. Invite users — add additional user accounts from the Settings page

Additional Configuration

Database Configuration

BookBoss supports four database backends. Choose the one that fits your deployment:

DatabaseBest for
SQLiteSingle-user, low maintenance, simple setups
PostgreSQLMulti-user, production deployments
MariaDBExisting MariaDB infrastructure
MySQLExisting 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

  1. Drop a file into the directory configured as BOOKBOSS__IMPORT__BOOKDROP_PATH
  2. 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.
  3. Metadata is extracted — for EPUB files, embedded metadata is read from the OPF inside the archive; other formats fall through to provider lookup
  4. 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.
  5. 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:, or tag: 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:

RuleDescription
Reading statusMatch by read/reading/unread/etc.
AuthorMatch books by a specific author
SeriesMatch books in a specific series
Genre / TagMatch books with a specific genre or tag
LibraryMatch 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:

StatusMeaning
UnreadDefault — book has not been read
ReadingCurrently reading
PausedReading paused
RereadingReading again
ReadFinished reading
AbandonedStopped 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:

  1. Click Create Library.
  2. 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

  1. Go to your Profile page

  2. Your OPDS password is auto-generated — copy it, or click Regenerate to create a new one

  3. 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/

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:

FeedURLDescription
Root catalog/opds/Entry point; shows “Libraries” nav entry for users with 2+ libraries
Default library/opds/allBooks in the user’s default library (not the full catalogue)
Libraries/opds/librariesNavigation 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/shelvesBrowse by shelf
Authors/opds/authorsBrowse by author
Series/opds/seriesBrowse by series

Each book entry includes download links for all available formats and cover images.

Note: /opds/all is 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

  1. Go to your Profile page
  2. Click Add Device and give it a name
  3. 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:

  1. Edit the companion shelf’s filter rules to select the books you want to sync (e.g. by reading status, author, genre, or tag)
  2. 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

VariableDescriptionDefault
BOOKBOSS__DATABASE__DATABASE_URLDatabase connection string (required)

See Database Configuration for connection string formats and examples.

Encryption

VariableDescriptionDefault
BOOKBOSS__ENCRYPTION_SECRETEncryption key for sensitive data (OPDS passwords) (required)

Frontend

VariableDescriptionDefault
BOOKBOSS__FRONTEND__LISTEN_IPIP address the web server listens on0.0.0.0
BOOKBOSS__FRONTEND__LISTEN_PORTPort the web server listens on8080
BOOKBOSS__FRONTEND__BASE_URLPublic-facing base URL (used for Kobo sync URLs)http://0.0.0.0:8080

Library

VariableDescriptionDefault
BOOKBOSS__LIBRARY__LIBRARY_PATHPath where approved book files are stored (required)

Import

VariableDescriptionDefault
BOOKBOSS__IMPORT__BOOKDROP_PATHDirectory to watch for new e-book files (required)
BOOKBOSS__IMPORT__SCAN_INTERVAL_SECSHow often (seconds) to scan the bookdrop directory60
BOOKBOSS__IMPORT__WORKER_POLL_INTERVAL_SECSHow often (seconds) the import worker polls for jobs5

Metadata Providers

VariableDescriptionDefault
BOOKBOSS__METADATA__HARDCOVER_API_TOKENAPI token for Hardcover (primary metadata provider)
BOOKBOSS__METADATA__GOOGLEBOOKS_API_TOKENAPI 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)

VariableDescriptionDefault
BOOKBOSS__API__GRPC_LISTEN_IPIP address the gRPC server listens on0.0.0.0
BOOKBOSS__API__GRPC_LISTEN_PORTPort the gRPC server listens on8081

Database Admin (just commands)

These variables are used by just create-database and just database, not by BookBoss itself:

VariableUsed by
PGUSERjust create-database, just database
PGPASSWORDjust create-database, just database
PGDATABASEjust create-database, just database
PGADMINUSERjust create-database
PGADMINPASSWORDjust 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-exports
  • model.rs (or model/) — domain types (Foo, NewFoo, FooId, FooToken)
  • repository.rs (or repository/) — FooRepository trait (port)
  • service.rsFooService trait + 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:

  1. Hardcover — primary provider (GraphQL API); returns metadata, cover, ratings, genres
  2. Open Library — ISBN-based and title search fallback; returns metadata and cover
  3. 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_PATH on a timer (or via manual trigger), hashes new files, and enqueues ImportJob records

The import worker runs via the core job system (CoreSubsystem/JobWorker):

  • Processes ImportJob records through the PipelineService: 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/rejected
  • JobsChanged — 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

  1. Create a directory under crates/core/src/ (e.g. order/)
  2. Add mod.rs, model.rs, repository.rs, service.rs
  3. Re-export from mod.rs
  4. Register the module in lib.rs
  5. 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

CommandDescription
just buildBuild the project
just runRun the application (Dioxus dev server)
just fmtFormat code (nightly rustfmt + Prettier)
just clippyRun Clippy lints (nightly)
just cleanClean the workspace
just depsUpdate Rust crate dependencies
just tailwindcssRegenerate Tailwind CSS
just configEdit encrypted configuration (sops)
just install-toolsInstall mise, nightly Rust, wasm32 target
just bundleBundle web + server for release

Testing

CommandDescription
just component-testsComponent/unit tests only (nextest)
just quick-testComponent + Postgres + SQLite integration tests
just testAll tests (all database backends)
just integration-testsAll integration tests (requires Colima)
just postgres-integration-testsPostgres integration tests
just sqlite-integration-testsSQLite integration tests
just mysql-integration-testsMySQL integration tests
just mariadb-integration-testsMariaDB integration tests
just instaRun insta snapshot tests
just insta-reviewReview insta snapshot deltas

Database

CommandDescription
just databaseDatabase admin
just create-databaseCreate the Postgres database and user

Documentation

CommandDescription
just docs-serveServe documentation locally (mdBook)
just docs-buildBuild documentation

Release

CommandDescription
just changelogGenerate changelog from git history
just release VERSIONCreate a release

Conventions

Version Control

This project uses jujutsu (jj), not git directly.

Key commands:

CommandDescription
jj commitCommit current changes
jj describe -m "…"Update the working copy description
jj newStart a new change
jj logShow history
jj statusShow 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

CrateApproach
core, api, databasethiserror 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-nextest as the test runner (just test)
  • Use cargo-insta for 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:

  1. just fmt — format code
  2. just clippy — lint (run separately from fmt, not chained)
  3. just component-tests — verify tests pass
  4. jj 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, and use_state are 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

  • AuthSession is stored in request extensions by AuthSessionLayer
  • 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/rejected
  • JobsChanged — 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:

VariableUsed by
PGUSERjust create-database, just database
PGPASSWORDjust create-database, just database
PGDATABASEjust create-database, just database
PGADMINUSERjust create-database
PGADMINPASSWORDjust create-database
BOOKBOSS__DATABASE__DATABASE_URLMigrations, 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/file or sqlite::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