# 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 a member with `is_admin` set.** ~~The admin is not a user.~~ *Reversed.* The original call — env credentials, Basic Auth, its own loopback listener — bought network isolation at the price of a second port to tunnel and proxy, and a second credential in the password manager. Basic Auth also sat outside the `SameSite` protection the member cookie already had, and left admin actions with no actor to log. One boolean column reuses the session, the login rate limiter, the ban-drops-sessions path and CSRF protection that all existed anyway. The costs the original entry named are real but small here: seeding is `seedAdmin` on an empty database, and the only lockout rule is that an admin cannot ban themselves. Banning a *second* admin is allowed — with one admin per installation there is no last-admin case to protect. 9. **No moderator tier.** A four-level role enum was considered and dropped: nothing in the admin surface distinguishes a superadmin from an admin, and moderator is a second column on the day somebody needs to resolve reports without also being able to reset passwords. 10. **The admin recovery endpoint is dropped.** The old app had a key-gated credential reset with a `qwerty123` default in `docker-compose.yml`. There is no route, no key and no default: an admin who loses their password is reset from the database, the same as any locked-out member. 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 `