Vellum/README.md
mithral 7a5584a83f docs: record phase 0-3 build progress and status
README status table, full env var reference including admin bootstrap and
cookie security, package layout, and build commands. Plan doc updated with a
progress log, the deviations from plan and why, and the gaps carried forward.
2026-10-09 16:42:53 -04:00

129 lines
4.8 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 | **done** |
| **2** | Scanner, items, assets | **done** |
| **3** | EPUB reader + progress | **done** |
| **UI** | Svelte app (login, libraries, items, reader, users) | **done** |
| 2b | FTS5 search | 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 |
What works today: an administrator creates a library pointing at a server
folder, scans it, and users sign in and read EPUBs in the browser with their
position saved per account. Accounts, per-library grants, cover extraction,
range-based file serving, and the container all work. Nothing else does 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 (covers, pages) |
| `VELLUM_MEDIA_DIRS` | *(none)* | Read-only library roots, colon-separated |
| `VELLUM_ADMIN_USER` | *(none)* | Creates the first admin, **only on an empty database** |
| `VELLUM_ADMIN_PASSWORD` | *(none)* | Password for that first admin (min 8 chars) |
| `VELLUM_COOKIE_SECURE` | `false` | Set `true` when serving over TLS |
| `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 |
The first admin is created only when no users exist, so setting these on an
established install does nothing. If the database is empty and they are unset,
the server logs a warning and refuses to invent a default password.
## Layout
```
cmd/vellum/ main — wiring and lifecycle only
internal/
core/ config, logging, database, migration runner
httpx/ shared JSON helpers
httpapi/ router, middleware, static SPA serving
auth/ argon2id, sessions, grants, bootstrap
library/ library CRUD, media-type registry, scanner
media/ generic item endpoints, cover-extractor registry
progress/ the polymorphic per-user progress store
stream/ range-capable file serving
modules/ebooks/ EPUB parsing and cover extraction
web/ Svelte app; dist/ is embedded via web/embed.go
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 build # builds the frontend, then the Go binary with it embedded
make build-go # Go only, when the frontend is unchanged
make check # gofmt + vet + test
make run # run the server on :7788
cd web && pnpm dev # frontend dev server, proxies /api to :7788
```