Running `pnpm dev` alone gave ECONNREFUSED on every request, because nothing was listening on :7788. Two separate causes: - the backend simply was not running, and - Vite bound [::1]:5173 while the proxy target was "localhost", which Node resolves to ::1 first. The Go server binds IPv4, so even with the backend up the proxy could fail before falling back. - vite.config.ts: proxy target is now 127.0.0.1 explicitly and overridable via VELLUM_API; server.host is true so both stacks are served. - scripts/dev.sh + `make dev`: builds the backend, waits for /healthz, starts Vite, and stops the backend on exit. Ctrl-C tears down both. - README documents that running Vite alone needs the backend already up.
158 lines
6.4 KiB
Markdown
158 lines
6.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 | **done** |
|
|
| **2** | Scanner, items, assets | **done** |
|
|
| **2b** | FTS5 search | **done** |
|
|
| **3** | EPUB reader + progress | **done** |
|
|
| **4** | CBZ comic reader | **done** |
|
|
| **PDF** | PDF reading (pdf.js) | **done** |
|
|
| **5** | Job queue, metadata (Open Library, Google Books, MusicBrainz, ComicVine, IGDB) | **done** |
|
|
| **6a** | Audiobooks + music playback | **done** |
|
|
| **6c** | Photos + thumbnails | **done** |
|
|
| **7** | Games: ROM scanning, platforms, cloud saves, EmulatorJS | **done** |
|
|
| **UI** | Svelte app: home, browse, item pages, readers | **done** |
|
|
| 6b | Music smart playlists | not started |
|
|
| — | Video (stays on Jellyfin) | deferred |
|
|
|
|
What works today: an administrator adds libraries pointing at server folders,
|
|
scans them, and optionally enriches metadata. Users sign in and read EPUBs and
|
|
PDFs, read CBZ comics, listen to audiobooks track-by-track, browse photos, and
|
|
play emulated games in the browser with their own cloud saves — with position
|
|
saved per account, per file.
|
|
|
|
**Six media types work: ebooks, comics, audiobooks, music, photos, games.**
|
|
Video is the only one not implemented, and it stays on Jellyfin on purpose.
|
|
|
|
Of the services in the existing stack, this covers **Stump, Kavita,
|
|
Audiobookshelf, Navidrome, and RomM**. Only Jellyfin (video) is left.
|
|
|
|
## 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_COMICVINE_API_KEY` | *(none)* | Enables comic metadata via ComicVine |
|
|
| `VELLUM_IGDB_CLIENT_ID` | *(none)* | Enables game metadata via IGDB |
|
|
| `VELLUM_IGDB_CLIENT_SECRET` | *(none)* | IGDB client secret (both required) |
|
|
| `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/file/cover/progress endpoints
|
|
progress/ the polymorphic per-user progress store
|
|
stream/ range-capable file serving
|
|
natsort/ natural ordering for pages and tracks
|
|
jobs/ table-backed job queue
|
|
metadata/ provider interface, Open Library, Google Books, enricher
|
|
search/ FTS5 queries
|
|
modules/
|
|
ebooks/ EPUB parsing and cover extraction
|
|
comics/ CBZ page listing and extraction
|
|
photos/ thumbnail generation
|
|
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 dev # backend + Vite dev server together, with hot reload
|
|
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 only; needs the backend running on :7788
|
|
```
|
|
|
|
`make dev` is the one to reach for while developing: it builds and starts the
|
|
backend, waits for it to answer, then starts Vite. Vite proxies `/api` to the
|
|
backend so the browser sees a single origin and sessions behave exactly as in
|
|
production. Ctrl-C stops both.
|
|
|
|
If you run Vite on its own, the backend must already be running, or every
|
|
request fails with `ECONNREFUSED` on `:7788`.
|