No description
Find a file
mithral 5c63932419 Phase 3: EPUB module, file streaming, per-user progress
- stream: serving goes through http.ServeContent, so Range/If-Range/
  If-Modified-Since are correct for every media type by construction
- progress: the polymorphic store. One row per user/item/file; the PK includes
  the file so multi-track audiobooks resume per track later. Finished is
  inferred when position reaches duration.
- media: generic item endpoints (detail, file, cover, progress, recent). Every
  one routes through auth.CanAccessLibrary; an unauthorised item 404s.
- covers: a per-kind extractor registry. Extraction is lazy and recorded in
  the assets table. A missing cover is a placeholder, never a 500.
- modules/ebooks: EPUB parsing (container.xml -> OPF -> metadata, spine, cover
  with three fallback strategies), cover extraction to /config, and lazy
  metadata parsing so parser fixes need no rescan.

Verified against a generated EPUB: title/2 authors/language/publisher/spine,
cover served as PNG, range request returned 206 with correct Content-Range,
progress round-tripped.
2026-10-09 16:36:27 -04:00
cmd/vellum Phase 3: EPUB module, file streaming, per-user progress 2026-10-09 16:36:27 -04:00
internal Phase 3: EPUB module, file streaming, per-user progress 2026-10-09 16:36:27 -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