# Levyraati A private music review club. Members submit songs — by file upload or YouTube link — and review each other's picks on a 1–100 scale. Other people's reviews of a song stay hidden until you've written your own. Invite-only, no public registration. Built for about ten friends. > **Status:** not built yet. This README describes the app defined in [docs/spec.md](docs/spec.md); > commands here are the intended interface, not a record of anything that runs today. ## Docs | File | What's in it | |---|---| | [CONTEXT.md](CONTEXT.md) | The glossary — every domain term, in English and Finnish | | [docs/spec.md](docs/spec.md) | What the app does: rules, pipeline, routes, API contract, schema | | [docs/decisions.md](docs/decisions.md) | Why it is that way. Append-only | | [docs/theme.md](docs/theme.md) | The visual language: tokens, type, and what differs from the theme handoff | | [docs/later.md](docs/later.md) | Deliberately not in v1, with the reasoning kept | | [docs/deployment.md](docs/deployment.md) | Running it on a server: the compose file, releases, upgrades, backups | ## Branches and releases - **`main`** — released code only. Every commit on it is something that ran in production, or is meant to. Tagged at each release. - **`dev`** — current development, and whatever nightly builds get made. Work happens here and reaches `main` by merge at release time. Versions are **CalVer: `YYYY.MM.DD-N`**, where `N` is the build number for that day, starting at 1. The version is injected at build time, so no file in the repo carries it: ```sh git switch main && git merge --no-ff dev git tag 2026.07.31-1 VERSION=$(git describe --tags --exact-match) docker compose build app docker compose up -d app ``` A plain `go build` reports `dev`, which is the honest answer for a local binary. The running version appears in the footer, in the startup log line, and in `GET /healthz` — so "what is actually deployed" is answerable without an SSH session. The app never sends email — there is no verification, no password reset link, and no notifications. Members have an address because it is their login and because mail is a planned feature. ## Stack Go, SQLite, `html/template`, HTMX + Alpine. Audio is converted with ffmpeg and downloaded with yt-dlp. One binary, one origin, one container — there is no separate frontend and no database server to deploy. Go dependencies: `modernc.org/sqlite` and `golang.org/x/crypto`. The SQLite driver is pure Go, so the build stays `CGO_ENABLED=0`. No Node, no npm, no bundler. ## Running it ```sh cp .env.example .env # then edit — ADMIN_PASSWORD has no default and the app won't start without it docker compose up -d ``` Migrations apply themselves at startup, before the server accepts connections. The first launch creates no users: log into the admin panel and mint an invite. ### Configuration | Variable | Default | Notes | |---|---|---| | `DB_PATH` | `$STORAGE_DIR/levyraati.db` | The SQLite file. Created on first start | | `ADMIN_USER` | `admin` | Admin panel username | | `ADMIN_PASSWORD` | — | **Required.** No default; the app refuses to start without it | | `ADDR` | `:8080` | Member-facing listener | | `ADMIN_ADDR` | `127.0.0.1:8081` | Admin listener. Keep it on loopback. Under Compose it binds `:8081` inside the container and is published only to the host's loopback | | `STORAGE_DIR` | `./storage` | Audio, avatars, in-flight conversions | | `SECURE_COOKIES` | `true` | Set `false` for local development over plain HTTP | | `PUBLIC_URL` | — | Public address of the member site, e.g. `https://levyraati.example.com`. Used to build invite links in the admin panel; unset gives relative links | ### Local development ```sh export ADMIN_PASSWORD=dev SECURE_COOKIES=false go run . ``` Requires Go 1.25+, plus `ffmpeg`, `ffprobe`, and `yt-dlp` on `PATH`. There is nothing to start first: the database is a file under `./storage`, created on the first run. Tests get a fresh database file in a temp directory each, so they need no setup and touch nothing: ```sh go test ./... ``` Templates, stylesheet, and migrations are embedded with `embed.FS`, so a rebuild is needed to see template changes. `go build && ./levyraati` is the loop. ## Admin panel The admin is **not a user account**. It exists only as `ADMIN_USER` / `ADMIN_PASSWORD`, authenticates with HTTP Basic Auth, and is bound to loopback so it is not reachable from the internet. Reach it through an SSH tunnel: ```sh ssh -L 8081:127.0.0.1:8081 you@server # then open http://localhost:8081 ``` From there: mint invites, reset member passwords, ban members, delete songs, read issue reports. **Lost the admin password?** Edit `.env` and `docker compose restart app`. There is no recovery endpoint and no recovery key — the credentials are the environment. ## Operations ### yt-dlp goes stale yt-dlp needs regular updates to keep working against YouTube. It comes from Alpine's community repository, whose active branch tracks upstream closely, so rebuilding is how you update it: ```sh docker compose build --no-cache app && docker compose up -d app ``` **Rebuild monthly.** When submissions start failing with download errors, this is the first thing to try. Failures are shown to the submitter with the yt-dlp error attached, so they're visible without reading logs. ### Logs ```sh docker compose logs -f app ``` JSON to stdout, nothing else. There is no log table and no log viewer in the app. ### Backups `./storage` holds everything: audio files, avatars, and `levyraati.db`. It is a bind mount, so a copy of that one directory is the whole backup. `storage/tmp/` is in-flight conversions and is safe to skip; it's cleared on startup anyway. Copying the file while the app is running is not a backup — WAL means the latest writes live in a sidecar file. Ask SQLite for a consistent snapshot instead: ```sh docker compose exec app sqlite3 /storage/levyraati.db ".backup '/storage/tmp/backup.db'" gzip -c storage/tmp/backup.db > backup-$(date +%F).db.gz && rm storage/tmp/backup.db ``` ### Database shell ```sh docker compose exec app sqlite3 /storage/levyraati.db ``` ## Layout Go source is flat at the repository root, one package. Beyond that: `templates/` and `static/` are embedded assets, `migrations/` holds numbered `.sql` files applied in order at startup, and `testdata/` holds the golden JSON files that guard the API contract, plus `ytdlp-noose.json` — a real `yt-dlp -J` dump of an ordinary upload, used to test metadata prefill against a video that has no `track`, `artist` or `album` at all. ## Notes Downloading audio from YouTube is against YouTube's terms of service. This is a private app among a handful of friends; the decision is deliberate rather than accidental. ## Licence MIT — see [LICENSE](LICENSE).