# Decisions Append-only. Entries are never rewritten — if one is reversed, the reversal is a new entry that says so. What the app does today is in [spec.md](./spec.md); this is why. --- ## 2026-07-31 — the rewrite Levyraati existed as a Nuxt 4 app that reached v1.0-rc.3 and never launched. It is being rebuilt in Go and the Nuxt repo archived. The reason was proportion: no SSR or SEO requirement for a login-walled app for ten friends, no client state worth a reactive framework, and a dependency surface larger than the feature set. There is no data to migrate — everything in the old `pgdata` and `storage` was test data, so **the schema has no legacy to respect.** 1. **Go, `net/http`, `html/template`, `pgx`, plain SQL.** Dependency budget is `pgx` and `x/crypto`; anything else needs a reason. Go 1.22 routing patterns cover every route, so no router. Seven tables, so no ORM. 2. **Migrations run at startup**, before serving. One less deploy step. Carried over from the old app, which got this right. 3. **Docker Compose shape carried over**: app + postgres, `./storage` and `./pgdata` bind mounts, healthcheck-gated `depends_on`. 4. **No Tailwind, no bundler, no npm.** One hand-written stylesheet with CSS custom properties. The dark rock/metal theme — Oswald headings, orange/red accents — survives dropping Tailwind unchanged, as theme tokens rather than utility soup. 5. **Finnish only.** No locale JSON, no `T()`, no language cookie, no switcher. Strings go directly in templates. The audience is ten Finnish speakers. 6. **`bigserial` ids, not UUID.** No id-generation code, no `google/uuid` in a two-dependency budget, smaller indexes, and `/audio/{id}` validates by parsing an integer. Enumerable ids are not a threat model here. Session tokens stay random — those are secrets. 7. **stdout logging only.** The rejected alternative was mirroring every entry into a `logs` table for an in-app viewer: a custom handler, a buffered channel, a drain goroutine, drop accounting, a retention `DELETE`, a table and a filtered page — ~150 lines to avoid `docker compose logs`. If in-app visibility is ever wanted, build an *audit* view of domain events instead; those are queries over tables that already exist. 8. **The admin is not a user.** Env credentials, Basic Auth, its own loopback listener. This deletes the `role` column, first-launch seeding, admin sessions, the "cannot ban the last admin" rules, and every "exclude the admin" clause that would otherwise appear in user and stats queries. 9. **Same process, two listeners** — not a second binary. A management binary would need its own deploy and would race the startup migrations. Two listeners give the network isolation, which was the only real benefit. 10. **The admin recovery endpoint is dropped.** The old app had a key-gated credential reset with a `qwerty123` default in `docker-compose.yml`. The password is an env var now, so recovery is editing it and restarting. No route, no key, no default. 11. **Conversion runs in the background; nothing enters `songs` until it succeeds and the submitter confirms.** Costs a `submissions` table, buys a `songs` table where every row is a real song and no query filters on readiness. 12. **Metadata is read synchronously at submit, conversion asynchronously.** Otherwise prefill races the submitter's typing. This also revived the YouTube prefill cut earlier — the objection was the extra round trip, and the redesign removed it. 13. **Publish is an explicit click**, never automatic on conversion success. 14. **Native `