--- phase: 03-first-vertical-slice-genres-end-to-end plan: 01 subsystem: api tags: [postgres, gorm, gormigrate, jwt, hs256, servemux, middleware, genres] requires: - phase: 02-api-parity-harness-bootstrap provides: App-owned newTarget/seedHooks seam and recorded GET genres fixture - phase: 01-framework-kernel-foundation provides: party.Activate, backpack Publish/Lookup, compass SUMMER_ overlay, bonfire commands provides: - Shared pgx-stdlib *sql.DB plus GORM handle with ICU pl-PL locale check - Per-plugin gormigrate history tables and 15 canonical genre seed rows - surf Group/Get compiled onto net/http ServeMux with named middleware - bouncer HS256 verifier and jwt.auth / inv.must-change-password pipeline - JWT-protected GET /_fonoteka/api/v1/genres returning seeded data affects: [03-02, 03-03, 03-04, 05, 06, 07, 11] tech-stack: added: - github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/go-gormigrate/gormigrate/v2 v2.1.7 - gorm.io/gorm v1.31.2 - gorm.io/driver/postgres v1.6.3 - github.com/jackc/pgx/v5 v5.10.0 patterns: - One *sql.DB shared by GORM and app services; River listener pool is a Phase 11 seam - Optional pact capabilities discovered in Activate order - Named middleware resolved at Assemble; missing names fail boot with plugin ID and name - Generated main stays framework-generic; fonoteka.go/app.Handler is the in-process test seam key-files: created: - lagoon/connection.go - lagoon/migrations.go - lagoon/commands.go - surf/router.go - surf/serve.go - bouncer/jwt.go - bouncer/context.go - ../fonoteka.go/app/app.go - ../fonoteka.go/plugins/golem15/user/plugin.go - ../fonoteka.go/plugins/golem15/fonoteka/plugin.go - ../fonoteka.go/plugins/golem15/fonoteka/genre_handler.go - ../fonoteka.go/parity/genre_smoke_test.go modified: - pact/capabilities.go - internal/build/build.go - cmd/summer/main.go - go.mod - ../fonoteka.go/main.go - ../fonoteka.go/go.mod key-decisions: - "GORM and app services share one pgx-stdlib *sql.DB; the River LISTEN/NOTIFY pool is a Phase 11 seam and is not created in lagoon.Open" - "Generated app main stays framework-generic (lagoon.RuntimeCommands + surf.ServeCommand); fonoteka.go/app.Handler is the in-process boot seam for parity tests" - "Empty golem15.user.jwt.secret fails Boot; tests use a fixed test-only HS256 secret and do not issue tokens through a production API" patterns-established: - "Pattern: lagoon.Open/Use then Publish(*sql.DB, *gorm.DB) before Assemble; UserProvider Lookups GORM per request" - "Pattern: Group(prefix, surf.Use(names...), cb) compiles to ServeMux GET patterns; CORS wraps the mux so OPTIONS bypasses jwt.auth" - "Pattern: dedicated GenreDTO with initialized data slice, Cache-Control no-cache, private, album_count 0 until Plan 02" requirements-completed: [DATA-01, DATA-02, HTTP-01, HTTP-02, QA-04] duration: 17 min completed: 2026-09-17 --- # Phase 03 Plan 01: Boot the Postgres-backed JWT genre route Summary **Shared pgx-stdlib pool, per-plugin gormigrate, HS256 jwt.auth, and GET `/_fonoteka/api/v1/genres` serving the 15 seeded rows** ## Performance - **Duration:** 17 min - **Started:** 2026-09-17T17:47:49Z - **Completed:** 2026-09-17T18:04:34Z - **Tasks:** 2 - **Files modified:** 52 ## Accomplishments - Fresh ICU `pl-PL` Postgres migrates `golem15.user` then `golem15.fonoteka` with separate history tables and the 15 canonical genre slugs; libc/non-Polish databases fail before migrate; no `AutoMigrate`. - `surf` compiles Laravel-like groups onto `http.ServeMux` with recover → CORS → locale → named auth → must-change-password → org slot → no-op rate limit → handler. - `bouncer` verifies pinned HS256 tokens (`exp` + `sub` required) and `golem15.user` registers `jwt.auth`; `golem15.fonoteka` registers `inv.must-change-password` (423) and mounts `GET /_fonoteka/api/v1/genres`. - A valid test-only token returns 200 with a nonempty `data` array from Postgres; missing/malformed/expired/wrong-alg/bad-sig/missing-sub/unknown-user tokens never reach the handler. ## Task Commits Each task was committed atomically (framework `summercms.go` then app `fonoteka.go`): 1. **Task 1: Initialize the real app database through plugin migrations** - `d0d845052bb5de883646c699eeeb0d18aad23708` (feat, summercms.go) - `55f110e7d7968977c3abaa3d73905dc7408de30a` (feat, fonoteka.go) 2. **Task 2: Serve the seeded genre list behind real cross-plugin JWT middleware** - `4ee4c4a2fc27d644cdfac9131cb369e8617df861` (feat, summercms.go) - `d7915a80e655329bb726c64dc2488c9a1de9a48c` (feat, fonoteka.go) **Plan metadata:** (this commit) ## Files Created/Modified - `lagoon/connection.go` — shared pgx stdlib pool, ICU locale check, GORM on `postgres.Config{Conn: sqlDB}` - `lagoon/migrations.go` — per-plugin gormigrate sets and history tables - `surf/router.go` / `surf/serve.go` — Group/Get/Assemble and signal-aware `serve` - `bouncer/jwt.go` / `bouncer/context.go` — HS256 verifier and unexported user context key - `pact/capabilities.go` — HasMigrations, HasMiddleware, HasRoutes, HasModels - `../fonoteka.go/app/app.go` — in-process Handler seam used by genre smoke tests - `../fonoteka.go/plugins/golem15/user/` — users migration, jwt.auth, UserProvider - `../fonoteka.go/plugins/golem15/fonoteka/` — schema/seed, 423 gate, genre DTO handler - `../fonoteka.go/parity/genre_smoke_test.go` — valid token plus reject/423 cases; corpus stays pending ## Decisions Made - One `*sql.DB` for GORM and application queries; do not create a River listener pool until Phase 11. - Generated `main.go` cannot import `fonoteka/app`; `surf.ServeCommand` and `app.Handler` both call `surf.Assemble`. - Empty JWT secret fails Boot (`SUMMER_GOLEM15__USER__JWT__SECRET`); issuing stays out of production APIs. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Generated main cannot share the `fonoteka/app` import path** - **Found during:** Task 2 (serve / parity seam) - **Issue:** Plan text says generated main and parity tests call the same importable app package, but `summer build` is framework-generic and must not import Fonoteka. - **Fix:** Keep generated main on `lagoon.RuntimeCommands` + `surf.ServeCommand`; put `app.Handler(cfg, pluginIDs, sqlDB)` in `fonoteka.go/app` for tests. - **Files modified:** `internal/build/build.go`, `../fonoteka.go/app/app.go`, `../fonoteka.go/main.go` - **Verification:** `summer build` regenerates ServeCommand; genre smoke boots `app.Handler` - **Committed in:** `4ee4c4a` / `d7915a8` (Task 2) **2. [Rule 1 - Bug] `go mod tidy` drops `toolchain go1.27.0`** - **Found during:** Task 1 and Task 2 - **Issue:** tidy removes the pinned toolchain line required by project convention. - **Fix:** Restore `toolchain go1.27.0` after tidy in framework and plugin modules. - **Files modified:** `go.mod`, plugin `go.mod` files - **Verification:** modules still build under Go 1.27 - **Committed in:** Task 1 and Task 2 commits **3. [Discretion] JWT malformed/bad-signature copy from library source** - **Found during:** Task 2 - **Issue:** Plan asked to capture malformed/bad-signature PHP bodies on the isolated Phase 2 server before finalizing translations. - **Fix:** Used the php-open-source-saver/jwt-auth (Namshi) messages already named in CONTEXT D-10: `Wrong number of segments` and `Token Signature could not be verified.` - **Files modified:** `bouncer/jwt.go` - **Verification:** genre smoke table-driven 401 bodies - **Committed in:** `4ee4c4a` (Task 2) --- **Total deviations:** 3 auto-fixed (1 blocking, 1 bug, 1 copy-source discretion) **Impact on plan:** No scope creep. QA-04 parity-green remains Plan 03; this plan keeps the corpus pending. ## Issues Encountered None that blocked the slice. `go mod tidy` dropping toolchain is now a known restore step. ## User Setup Required None - no external service configuration required beyond the existing Postgres ICU `pl-PL` database and `SUMMER_DATABASE__DSN` / `SUMMER_GOLEM15__USER__JWT__SECRET` env vars documented in `../fonoteka.go/README.md`. ## Next Phase Readiness Ready for `03-02-PLAN.md` (active-collection album counts, `non_empty` validation, Polish ordering). Do not mark the genres corpus route `ported` until Plan 03. QA-04 stays pending until that parity diff is green. ## Self-Check: PASSED - Key files exist on disk (`lagoon/connection.go`, `surf/router.go`, `bouncer/jwt.go`, `../fonoteka.go/app/app.go`, `../fonoteka.go/parity/genre_smoke_test.go`) - `git log --grep=03-01` returns Task 1 and Task 2 commits - Acceptance: 15 seeded genres, ICU locale fail, shared `*sql.DB`, JWT 200/401/423, missing middleware names plugin+name, framework does not import Fonoteka, `serve` on both binaries, `go vet`/`go test ./...` green in both repos --- *Phase: 03-first-vertical-slice-genres-end-to-end* *Completed: 2026-09-17*