← All projects

Project field notes

Marginalia

A companion web app for Kavita that adds Discord notifications, group reads, series requests, reading history and goals, achievements, a browsable library, recommendations, and admin-side library tooling — all on top of your existing Kavita database, read-only.

activev0.6.6Updated
Quick start./Marginalia --kavita-db /path/to/kavita.db

Walkthroughs, both running against a demo library — probably the fastest way to tell whether this is for you:

  • As a user → — the home feed, browsing, following a series, notifications, discovery, requests, group reads, history and goals, achievements and Year in Review.
  • As the admin → — sign-in and SSO, path translation, metadata databases, webhook routing, the request queue, collection gaps, background jobs, backups and health.

New in 0.6.6

The biggest release since launch, and almost all of it is about reading with other people:

  • Group Reads — read a series or a Kavita reading list together on a shared schedule, with a progress board fed straight from Kavita, per-milestone discussions, shared highlights and pages, Discord reminders, and a Book Club achievement. Walkthrough →
  • Requests — ask for a series the server doesn’t have, in a specific edition and library, and get told when it arrives.
  • Year in Review — a look back at your reading year, opening each 22 December. Past years are rebuilt from your Kavita history.
  • A new home page — what you’re in the middle of, a two-week “what’s new” feed, and a side rail with your next group-read deadline, invitations, streak and requests.
  • Auto-watch, reading-session summaries on Discord, and Kavita-matching progress percentages.
  • For admins: Collection Gaps, a nightly check for missing volumes in series you already have.

0.6.5 added daily reading and yearly book goals; 0.6.2 added “who else is reading this” on the series page.

What it does

Kavita is a great reading server. It just doesn’t tell you when new chapters land, track much about how you read, or help you pick what to read next — and if several people read off your server, it gives them no reason to care that they’re doing it together.

Marginalia bolts all of that on. It runs beside Kavita, reads your existing kavita.db read-only, and keeps its own separate database for everything it adds. Your Kavita install is never modified — no schema changes, no write locks, nothing to roll back if you delete Marginalia.

Everyone signs in with their existing Kavita username and password. There is no second set of accounts to create, and no per-user setup: point it at the database, and every Kavita user can log in immediately.

Is this for you?

Probably yes if:

  • You run Kavita and want a Discord ping when a series you follow gets new chapters.
  • You want “catch up” notifcations that remind you to go back and read once there is some backlog in your series.
  • Multiple people read off your server and you’d like reading stats, streaks, a bit of friendly competition — or to actually read the same series together on a schedule.

Probably not if:

  • You want an in-browser reader today. Marginalia doesn’t replace Kavita’s reader — you keep using Kavita for reading. (One is planned (maybe); see Why 0.5.0.)
  • You want it to fix your metadata inside Kavita. Marginalia reads Kavita; it never writes back.
  • You only have a handful of books and read alone. It’ll work, but most of this is aimed at bigger libraries and more than one reader.

For readers

Discord notifications

The core loop: go to My Series, toggle Watch for new on anything you’re reading, and set a Discord webhook. A background worker polls for new chapters, and when one lands you get a ping with the series name, the chapter count, and your username.

Turn on auto-watch and series you start reading are added to the watchlist for you. There are five independent event types you can subscribe to:

EventWhat it does
New Chapters ReleasedLibrary-wide firehose — any series you can access, not just watched ones
New Series AddedSomething new appeared in a library
Catch-up RemindersPer-series threshold: you set 90%, and it pings once you fall back below it
Publishing Status ChangesA watched series goes ongoing → completed, hiatus, cancelled
Library UpdatesScan-level activity

Messages name the library a series is in. You can also opt into a reading-session summary — a Discord message with the cover each time you finish a session.

Webhooks can be global (one URL for everything) or overridden per series. Admins can set quiet hours so nothing fires at 3am.

Catch-up reminders are the one worth explaining: when you set a threshold, Marginalia records your current read percentage as a baseline and stays quiet. It pings you later, once enough new content exists that your progress has dropped to that threshold. It’s for series you don’t want to read one chapter at a time while some backlog builds up.

The home page

Signing in lands on a dashboard: the series you’re in the middle of (with how far along you are and how many new chapters have arrived), a “What’s new” feed covering the last two weeks, and a “Because you read…” shelf from Discover. The side rail carries your next group-read deadline, any invitations, your reading streak and latest achievement, and your open requests. Group-read posts in the feed show who posted but never what they said, so nothing is spoiled before you reach it.

Browse and Discover

Browse is a shelf-based view of your library — continue reading, recently added, then a shelf per library — with variable card sizes per shelf and a blurred-cover hero on the series detail page. There is also a dedicated “All Series” tab that lets you view your entire library and go into detailed series views.

Discover is a personalized recommendations hub. It pulls “readers also like” data from MangaBaka for the series you’ve actually been reading, then filters it down to series already in your library that you haven’t started. No wishlist of things you don’t own — just the stuff sitting on your own server that you never got around to. Recommendations are precomputed in the background so the page loads instantly, and an admin can switch the hub off entirely. The Highest rated tab is split into rating bands (70–74 up to 90+), each with a count, and can be ordered by rating, publication year, or a daily shuffle.

The series page also shows who else on the server is reading it and how far along they are — only for readers whose profile is public.

Requests

Ask for a series the server doesn’t have yet — always a specific edition and target library, never a bare title, because the manhwa and the light novel of the same name are different requests. Others can add a “me too” instead of filing a duplicate, the admin can accept or decline with a note, and everyone who asked is told when the series actually lands in that library, however it got there.

Group Reads

A few readers, one series or Kavita reading list, and a shared schedule. Progress is read straight from Kavita, so there’s no check-in button, and the board only ever shows percentages — never which chapter someone is on. Each milestone gets its own discussion: general thoughts, member questions, one-line quick takes with a rating, and highlights or bookmarked pages you made in Kavita, attached from your own account. Shared pages are copied, so they outlive the Kavita bookmark.

Falling behind never locks you out: a reader who hasn’t finished a stretch gets a spoiler notice and a one-click way in. Groups can be public or private (invite or join code), post reminders to their own Discord channel, and can be archived as a snapshot once they’re done. Finishing on schedule earns the Book Club achievement.

Reading history and bookmarks

Reading history is a week-by-week view of what you read, paginated back through past windows.

Goals: set a daily reading goal in minutes (with an optional nudge at a time of day if you’re still short) and a yearly goal for books finished. Time comes from Kavita’s own reading sessions, so it counts reading rather than marking things read, in your own time zone. Both show on Reading History, and there’s a Goal Streak achievement for consecutive days.

Year in Review opens each 22 December: books, chapters and pages finished, hours, new series started, and what you read most. Earlier years are always available from your profile, rebuilt from your Kavita history if you were reading before Marginalia was installed.

Bookmarks is a gallery of saved reading moments — page snapshots you can reopen, annotate with Markdown notes, and publish behind a share link (subject to a per-user cap the admin sets).

Achievements

Over 160 achievements and 700 tiers — reading milestones, streaks, tiered badges on your profile, plus server-wide aggregate ones that everyone on the install contributes to. Some carry monthly or seasonal deadlines, and an unlock that came from a particular series names it.

There’s also a WoW-style feat of strength class: achievements that are completely invisible until you earn them. The current batch includes per-series “Caught Up” feats, judged against MangaBaka’s live chapter counts rather than a hardcoded number, so they stay accurate as a series keeps publishing.

Users can optionally expose a public achievement profile on a share link.

Details that matter in practice

  • Per-user timezone, captured at first login and overridable in settings. Date- and hour-based achievements evaluate in your local time, not UTC.
  • Dark mode, with a navbar toggle and no flash of the wrong theme on load.
  • Scan summaries — a persisted record of exactly which series changed during each Kavita library scan, with optional webhook digests. Marginalia waits for a scan to settle before summarizing, so one scan produces one summary instead of a dozen.
  • First-login walkthrough that introduces the app, and shows returning users only the slides for features added since they last saw it.

For admins

Sign-in, including SSO

Password login checks the hash in Kavita’s own user table — same credentials, no sync, no second account. Kavita’s per-app login switch is respected, so a disabled account stays disabled here.

For installs where Kavita hands authentication to an identity provider, Marginalia registers as its own OpenID Connect relying party against that same provider (authorization code + PKCE). Turn it on under Server → Authentication, enter the authority, client id, and secret, and it applies immediately — no restart. Readers get a Sign in with … button above the password form.

Because the Kavita database is read-only, an SSO sign-in is only ever matched to an account that already exists — never used to create one. The reliable link is the sub claim against the OidcId Kavita records the first time someone signs into Kavita through the provider. An email-match fallback exists but ships off, because turning it on means anyone controlling an address at the provider can reach the Kavita account holding that address.

Library inventory

Scan your libraries into a searchable inventory, translate Kavita’s paths to local ones, and review or correct how each series was matched against the metadata database. This is what feeds series matching everywhere else in the app.

Offline metadata databases

Marginalia downloads and maintains local copies of the metadata databases used for matching, so scanning thousands of files doesn’t mean thousands of API calls:

  • MangaBaka for manga, with a weekly checksum-based refresh that only re-downloads on an actual change.
  • Comics.org (GCD) and a ComicVine dump for comic libraries.

Each library is assigned one active provider, so a manga library and a comics library can match against different sources on the same install.

Requests and collection gaps

Reader requests land in an admin queue with the edition, target library and everyone who asked; there’s a per-reader cap on open requests. Collection Gaps is a nightly check for missing volumes and chapters in series already on the server, measured against MangaBaka’s English print run where it knows one. Findings can be dismissed one at a time, which also hides them from the reader-facing series page.

Server tooling

An admin area covering webhook delivery and quiet hours, base URLs, users and login access, usage stats, a log viewer, background-job cron schedules, and per-feature visibility toggles.

Cron schedules fire in a configurable server timezone rather than UTC, so “3am” means 3am where the server actually lives.

Health monitoring: subsystems report into a central registry. A critical failure drops the app onto a maintenance page that tells you what broke, rather than crashing or silently serving wrong data. There’s a machine-readable /health endpoint for Uptime Kuma and friends, plus a status webhook for operational events.


Getting started

Marginalia ships as a self-contained binary — no .NET runtime to install — for linux-x64, linux-arm64, win-x64, win-arm64, osx-x64, and osx-arm64. Grab the archive for your platform from the download page, extract it, and run:

./Marginalia --kavita-db /path/to/your/kavita.db

The path is saved after the first run, so you only pass it once — after that it’s editable in the web UI under Server → Base URLs. Open the URL from the console and sign in with your Kavita credentials. The first admin to sign in runs the setup wizard; everyone else waits on a “setup in progress” page until it’s done.

Docker

The container image lives on GitHub’s registry at ghcr.io/dieseltech/marginalia (multi-arch, amd64 and arm64).

First, copy over the .env.example and rename it to .env. Make sure to modify it to have your paths and TZ.

.env:

# Copy this file to `.env` and edit the values, then `docker compose up -d`.

# Host timezone (IANA name, e.g. America/New_York, Europe/London).
TZ=Etc/UTC

# Host port to reach Marginalia on. The container always listens on 8080.
MARGINALIA_PORT=5000

# --- Host paths -------------------------------------------------------------
# Path to your Kavita config directory (the folder that contains kavita.db).
KAVITA_CONFIG=/path/to/kavita/config

# Path where Marginalia keeps its own data (its DB + the managed MangaBaka DB).
# Create an empty folder for this.
MARGINALIA_DATA=/path/to/marginalia/data

# --- Library paths ----------------------------------------------------------
# The HOST folders for your libraries. The container-side paths in compose.yml
# (/manga, /comics, /books) must match the paths Kavita stores in its DB — see
# the note at the top of compose.yml.
MANGA_PATH=/path/to/manga
COMICS_PATH=/path/to/comics
BOOKS_PATH=/path/to/books

docker-compose.yaml:

# Marginalia — public docker-compose.
#
# Marginalia is a companion app for a running Kavita instance: it reads Kavita's
# database (read-only) plus your library files, and layers on discovery,
# achievements, Discord summaries, and library tooling. It does NOT replace
# Kavita and never writes to Kavita's DB.
#
# Copy .env.example to .env, fill in the paths, then: docker compose up -d
#
# ---------------------------------------------------------------------------
# CRITICAL — path parity with Kavita
# ---------------------------------------------------------------------------
# Kavita stores absolute library paths in its own database (whatever your Kavita
# container sees, e.g. /manga). Marginalia resolves series folders using those
# exact stored paths, so the CONTAINER-side path of each library mount below
# MUST match the path Kavita has for that library. Change the host (left) side
# freely; keep the container (right) side identical to Kavita's.
# ---------------------------------------------------------------------------

services:
  marginalia:
    image: ghcr.io/dieseltech/marginalia:latest
    container_name: marginalia

    ports:
      # Host port : container port. Marginalia listens on 8080 inside the
      # container; expose it wherever is free on your host.
      - "${MARGINALIA_PORT:-5000}:8080"

    environment:
      # Kavita's config dir is mounted read-only at /kavita; kavita.db sits at
      # its root. ReadOnly is enforced at the SQLite driver level too.
      ConnectionStrings__KavitaConnection: "Data Source=/kavita/kavita.db;Mode=ReadOnly;Cache=Shared"
      # Marginalia's own DB on the writable /data volume; the managed MangaBaka
      # database is written beside it.
      ConnectionStrings__AppConnection: "Data Source=/data/kavita-discord.db"
      TZ: "${TZ:-Etc/UTC}"

    volumes:
      # Kavita config dir (contains kavita.db), read-only.
      - ${KAVITA_CONFIG}:/kavita:ro

      # Marginalia's persistent data (its DB + the managed MangaBaka DB).
      - ${MARGINALIA_DATA}:/data

      # ---- Libraries: container paths MUST mirror Kavita's (see note above) --
      # Read-only is enough for browsing, discovery, and achievements. Add or
      # remove lines to match your Kavita libraries; each right-hand path must
      # equal what Kavita stores for that library.
      - ${MANGA_PATH}:/manga:ro
      - ${COMICS_PATH}:/comics:ro
      - ${BOOKS_PATH}:/books:ro

    restart: unless-stopped

Mounting the Kavita folder :ro is recommended. The app opens the database read-only anyway, and the mount flag makes that guarantee enforceable.

The one gotcha worth knowing about

The common awkward setup is Kavita in Docker, Marginalia on the host. Kavita’s database stores paths the way Kavita sees them, so a library reads as /comics while the files actually live at /mnt/storage/comics on the host. Marginalia looks for /comics, finds nothing, and scans come up empty.

Configure it once per library under Server → Inventory → Filesystem Translation and Scanning: pick the Kavita folder path, set Local System Path Override to the real host path. Leave it blank when both apps already see the same paths. ie; both apps running in docker.

This only affects the features that touch files on disk — library inventory and ingest. Notifications, history, achievements, browsing, and bookmarks all read the database and don’t care where the files physically live. And if a translated path doesn’t exist, the scan fails loudly and names the exact path it tried, so a typo surfaces immediately instead of silently scanning nothing.

Privacy

Marginalia doesn’t collect, sell, or share user data. There is no telemetry. Outbound connections are limited to two places:

  • Discord — only the webhook URL you configure. Nothing is sent if you don’t set one.
  • MangaBaka (api.mangabaka.org) — the server downloads the public manga database and fetches recommendation data. Those requests carry manga IDs only — never anything about you, your account, or what you read. Discover’s covers and titles are served from your own library, so browsers never contact MangaBaka directly.

MangaBaka is a third party, so their privacy policy applies to those requests. Turning Discover off under Server → Discover stops them entirely.

Under the hood

  • ASP.NET Core 10 (Razor Pages) — web framework
  • EF Core + SQLite — same engine as Kavita
  • ASP.NET Core Identity + OpenID Connect — auth against the Kavita user table, and SSO
  • SkiaSharp / SharpCompress / ZstdSharp — image processing, archive handling, and metadata-database decompression
  • Markdig + HtmlSanitizer — bookmark notes
  • Cronos + NodaTime — cron schedules in a real timezone
  • Serilog — structured logging behind the in-app log viewer
  • A custom Marginalia design system on Bootstrap 5

Why 0.5.0

The version number is deliberate. This is the first public release, and it’s a real one — everything above is built, in daily use, and shipping. But 0.5.0 is roughly half of what I want this app to be, and calling it 1.0 would be claiming otherwise.

What’s in the other half:

Library ingest — a scan → review → import pipeline for a folder of messy downloads: parse filenames with a deterministic tokenizer, match against the metadata database and your existing library, review the ambiguous ones in a folder-grouped worklist, then import unattended (extract, convert, place). It’s built and you can turn it on from the labs page, but it is the least finished and least tested thing in the app, it moves files around on your disk, and it isn’t why you should install 0.5.0. Treat it as a preview, point it at a copy of something you don’t care about, and expect it to change.

A built-in reader — being able to actually read in Marginalia and write progress back to Kavita. Designed, not started. It’s the biggest single thing standing between 0.5.0 and 1.0.

Everything else on this page, I consider done.

Licensing

Marginalia is closed source for the time being: the GitHub page exists to host the container image and track issues, not host the code. Binaries come from the download page.

Copyright © 2025–2026 DieselTech. All rights reserved. Licensing may change in the future; if it does, it’ll be announced here first.