Vellum/README.md
mithral eb1ad6d920 Phase 0: skeleton, config, migrations, health check, container
Bootstraps Vellum as a single Go binary with one SQLite database.

- core: env-driven config, slog logging, SQLite (WAL) with pooled pragmas,
  embedded migration runner keyed on PRAGMA user_version
- migrations/0001_core.sql: identity, media, user state, infra. Media items
  are global; all per-user state (progress, favorites, grants) is a join.
  One polymorphic progress table covers every media kind.
- httpapi: request-ID, structured request logging, panic recovery, JSON
  endpoints for /healthz and /api/v1/version
- multi-arch Dockerfile (pure-Go SQLite, CGO_ENABLED=0) with an entrypoint
  that honours PUID/PGID like the rest of the self-hosted stack
- tests for migration idempotency, FK enforcement, WAL, config, middleware

Media does not exist yet. Nothing is exposed beyond health and version.
2026-10-09 16:26:08 -04:00

107 lines
3.4 KiB
Markdown

# 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
```