docs(03-01): complete boot JWT genre route plan

Tasks completed: 2/2
- Initialize the real app database through plugin migrations
- Serve the seeded genre list behind real cross-plugin JWT middleware

SUMMARY: .planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-17 20:06:03 +02:00
parent 4ee4c4a2fc
commit 20bd8fe8fc
4 changed files with 200 additions and 22 deletions

View File

@@ -0,0 +1,174 @@
---
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*