No description
Find a file
mithral 5458b0a28a Phase 2: libraries and the file scanner
- library: CRUD with root-path resolution (absolute, symlink-free, must be
  readable). A library name is unique; the root is validated at creation.
- registry: media types declare their extensions; the core never imports a
  module. RegisterType is the extension point.
- scanner: walks a root, reconciles media_items/media_files in one
  transaction, skips unchanged files by size+mtime, and marks vanished files
  missing rather than deleting them (progress is the one thing here that
  cannot be regenerated). A single unreadable file never fails a scan.
- migration 0002: `missing` flags on items and files, plus indexes.
- endpoints: media-types, library list/get/create/delete, per-library items,
  and scan. Library reads go through the same grant chokepoint as everything
  else; an unauthorised library 404s rather than 403s so its existence is not
  leaked.
2026-10-09 16:33:56 -04:00
cmd/vellum Phase 2: libraries and the file scanner 2026-10-09 16:33:56 -04:00
internal Phase 2: libraries and the file scanner 2026-10-09 16:33:56 -04:00
migrations Phase 2: libraries and the file scanner 2026-10-09 16:33:56 -04:00
.dockerignore Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00
.gitignore Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00
compose.yml Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00
Dockerfile Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00
entrypoint.sh Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00
go.mod Phase 1: accounts, sessions, per-library grants 2026-10-09 16:31:24 -04:00
go.sum Phase 1: accounts, sessions, per-library grants 2026-10-09 16:31:24 -04:00
Makefile Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00
README.md Phase 0: skeleton, config, migrations, health check, container 2026-10-09 16:26:08 -04:00

Vellum

One application, one process, one database for every media type — ebooks, comics, audiobooks, music, photos, games, and video — behind a single account system with real per-user progress and per-library permissions.

A modular monolith. One Go binary, one SQLite file, one web frontend.

The full plan lives in ~/.opencode/plan/vellum.md.

Status

Phase Scope State
0 Repo, config, migrations, health, Docker done
1 Users, sessions, library grants not started
2 Scanner, items, assets, FTS5 search not started
3 EPUB reader + progress not started
4 Comic (CBZ/PDF) viewers not started
5 Metadata providers not started
6 Audiobooks, music, photos not started
7 Games not started
— Video (stays on Jellyfin for now) deferred

Phase 0 delivers a binary that boots, migrates its schema, serves a health check, and shuts down cleanly. No media features yet.

Quick start

sudo dnf install golang        # once; Go is not in the base image here
make build
make run
curl localhost:7788/healthz    # {"database":"ok","status":"ok",...}

Run with media roots configured:

VELLUM_MEDIA_DIRS=/mnt/storage/media/ebooks:/mnt/storage/media/comics make run

Configuration

All configuration is environment variables. Every one has a default, so the binary runs with no configuration at all.

Variable Default Meaning
VELLUM_LISTEN :7788 Bind address
VELLUM_DATA_DIR ./data SQLite database lives here
VELLUM_CONFIG_DIR ./config Regenerable cache (thumbnails, pages)
VELLUM_MEDIA_DIRS (none) Read-only library roots, colon-separated
VELLUM_LOG_LEVEL info debug, info, warn, error
VELLUM_LOG_FORMAT text text or json
VELLUM_SESSION_TTL 720h Login session lifetime
VELLUM_SHUTDOWN_GRACE 15s Graceful shutdown timeout

Layout

cmd/vellum/          main — wiring and lifecycle only
internal/
  core/              config, logging, database, migration runner
  httpapi/           router, middleware, handlers
migrations/          embedded SQL, applied in order at boot

Module rule: the core never imports a feature module; modules import the core and mount their own routes, tables, and scanner rules. Adding a media type is a new package plus a migration, never a core edit.

Schema rule: media items are global; everything user-specific is a join. Nothing may hold per-user state on a media row.

Database

SQLite in WAL mode, opened through modernc.org/sqlite — a pure-Go driver, so the binary builds with CGO_ENABLED=0 and cross-compiles without a toolchain per target.

Migrations are embedded and applied in filename order; applied files are recorded only by number in PRAGMA user_version, so never edit an applied migration — add a new one.

Back up by copying the database (or its directory); VACUUM INTO 'backup.db' takes a consistent snapshot while running.

Deploy

make docker                                  # podman build
podman compose up -d                         # see compose.yml

The container mounts /data (database), /config (cache), and /media (read-only). On SELinux hosts every bind mount needs :z — compose.yml already has it.

The application never writes to /media.

Development

make check      # gofmt + vet + test
make test