# 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 ```bash 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: ```bash 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 ```bash 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 ```bash make check # gofmt + vet + test make test ```