← All projects

Project field notes

Marginalia

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

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

See the screenshot tour → — every page of the app, running against a demo library. Probably the fastest way to tell whether this is for you.

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.
  • Multiple people read off your server and you’d like reading stats, streaks, and a bit of friendly competition.
  • Your Kavita users sign in through an identity provider (Authentik, Authelia, Keycloak, …).

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; 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.

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

(A sixth, System Notifications, is in the UI but not wired up yet — it says so on the page.)

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.

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.

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.

Reading history and bookmarks

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

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 30 families of per-user achievements — reading milestones, streaks, tiered badges — plus around 20 server-wide aggregate ones that everyone on the install contributes to. Some carry monthly or seasonal deadlines.

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.

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 rather than a promise.

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.

Fix 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.

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

Publishes are ReadyToRun-compiled for faster startup. Full Native AOT isn’t possible — Razor Pages needs runtime reflection.

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.

My vulnerability scanner says you are insecure!

If you are referencing the AngleSharp 0.17.1 version - Yes, I know. However we are not vulerable based on how I use this package.

Our only untrusted-HTML sanitization path is BookmarkNoteMarkdownService (public shared-bookmark notes). It renders Markdig with .DisableHtml() and uses a tag allowlist that excludes MathML entirely. The CVE requires injecting a live <annotation-xml> element, which this pipeline structurally prevents.

With no path to MathML, the exploit pipeline it shutdown before it even starts.

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.