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

@@ -36,8 +36,8 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b
### Data layer (DATA) ### Data layer (DATA)
- [ ] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener - [x] **DATA-01**: GORM on Postgres through one shared *sql.DB (pgx stdlib) with a separate small pgx pool reserved for River's listener
- [ ] **DATA-02**: Each plugin ships a gormigrate migration set with up and down; sets run in plugin dependency order with per-plugin version tracking; AutoMigrate is never the schema source - [x] **DATA-02**: Each plugin ships a gormigrate migration set with up and down; sets run in plugin dependency order with per-plugin version tracking; AutoMigrate is never the schema source
- [ ] **DATA-03**: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction - [ ] **DATA-03**: Models get timestamps, soft delete, and lifecycle hooks (beforeValidate, beforeCreate, beforeSave, beforeDelete, afterDelete) that can cascade soft deletes inside a transaction
- [ ] **DATA-04**: Relations cover belongsTo, hasOne, hasMany and belongsToMany with ordered results and dedicated pivot models carrying business columns (CollectionEditor role/granted_at/granted_by, album_artists sort_order) - [ ] **DATA-04**: Relations cover belongsTo, hasOne, hasMany and belongsToMany with ordered results and dedicated pivot models carrying business columns (CollectionEditor role/granted_at/granted_by, album_artists sort_order)
- [ ] **DATA-05**: Model rule strings (required, between, unique:table, nullable, integer, in) are validated on save via go-playground/validator with translated messages and a 422 error map shaped like Laravel's - [ ] **DATA-05**: Model rule strings (required, between, unique:table, nullable, integer, in) are validated on save via go-playground/validator with translated messages and a 422 error map shaped like Laravel's
@@ -50,8 +50,8 @@ Requirements for v1 (the Płytarium port). Each maps to roadmap phases. "User" b
### HTTP and routing (HTTP) ### HTTP and routing (HTTP)
- [ ] **HTTP-01**: Plugins register route groups on net/http ServeMux with typed params and regex constraints; unknown and malformed ids both return 404 on ownership-scoped resources - [x] **HTTP-01**: Plugins register route groups on net/http ServeMux with typed params and regex constraints; unknown and malformed ids both return 404 on ownership-scoped resources
- [ ] **HTTP-02**: Plugins register named middleware that other plugins reference by name; the pipeline order is recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler - [x] **HTTP-02**: Plugins register named middleware that other plugins reference by name; the pipeline order is recover, CORS, locale, auth group, must-change-password, org context, rate limit, handler
- [ ] **HTTP-03**: Three mutually exclusive auth groups share the same handlers with different route subsets: JWT under /_fonoteka/api/v1, personal scoped token under /api/v1/fonoteka, and public groups (onboarding, public/{token}, public-wishlist/{token}, invitation inspection) - [ ] **HTTP-03**: Three mutually exclusive auth groups share the same handlers with different route subsets: JWT under /_fonoteka/api/v1, personal scoped token under /api/v1/fonoteka, and public groups (onboarding, public/{token}, public-wishlist/{token}, invitation inspection)
- [ ] **HTTP-04**: A rate limiter supports named buckets keyed by a resolver (token id, IP, route param), stacking two limiters on one route, and ports Płytarium's seven named buckets and inline throttles 1:1 - [ ] **HTTP-04**: A rate limiter supports named buckets keyed by a resolver (token id, IP, route param), stacking two limiters on one route, and ports Płytarium's seven named buckets and inline throttles 1:1
- [ ] **HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor - [ ] **HTTP-05**: An auth guard registry lets plugins add guards (JWT, personal token, OAuth bearer) that all resolve to the same current-user accessor
@@ -172,8 +172,8 @@ Which phases cover which requirements. Updated during roadmap creation.
| I18N-01 | Phase 4 | Pending | | I18N-01 | Phase 4 | Pending |
| I18N-02 | Phase 7 | Pending | | I18N-02 | Phase 7 | Pending |
| I18N-03 | Phase 4 | Pending | | I18N-03 | Phase 4 | Pending |
| DATA-01 | Phase 3 | Pending | | DATA-01 | Phase 3 | Complete |
| DATA-02 | Phase 3 | Pending | | DATA-02 | Phase 3 | Complete |
| DATA-03 | Phase 5 | Pending | | DATA-03 | Phase 5 | Pending |
| DATA-04 | Phase 5 | Pending | | DATA-04 | Phase 5 | Pending |
| DATA-05 | Phase 5 | Pending | | DATA-05 | Phase 5 | Pending |
@@ -183,8 +183,8 @@ Which phases cover which requirements. Updated during roadmap creation.
| DATA-09 | Phase 5 | Pending | | DATA-09 | Phase 5 | Pending |
| DATA-10 | Phase 5 | Pending | | DATA-10 | Phase 5 | Pending |
| DATA-11 | Phase 5 | Pending | | DATA-11 | Phase 5 | Pending |
| HTTP-01 | Phase 3 | Pending | | HTTP-01 | Phase 3 | Complete |
| HTTP-02 | Phase 3 | Pending | | HTTP-02 | Phase 3 | Complete |
| HTTP-03 | Phase 6 | Pending | | HTTP-03 | Phase 6 | Pending |
| HTTP-04 | Phase 6 | Pending | | HTTP-04 | Phase 6 | Pending |
| HTTP-05 | Phase 6 | Pending | | HTTP-05 | Phase 6 | Pending |

View File

@@ -122,7 +122,7 @@ Plans:
**Wave 1** **Wave 1**
- [ ] 03-01-PLAN.md — Boot the Postgres-backed JWT genre route through both plugins - [x] 03-01-PLAN.md — Boot the Postgres-backed JWT genre route through both plugins
**Wave 2** *(blocked on Wave 1 completion)* **Wave 2** *(blocked on Wave 1 completion)*
@@ -348,7 +348,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 →
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Framework kernel foundation | 4/4 | Complete | 2026-09-16 | | 1. Framework kernel foundation | 4/4 | Complete | 2026-09-16 |
| 2. API parity harness bootstrap | 5/5 | Complete | 2026-09-17 | | 2. API parity harness bootstrap | 5/5 | Complete | 2026-09-17 |
| 3. First vertical slice — genres end to end | 0/TBD | Not started | - | | 3. First vertical slice — genres end to end | 1/4 | In Progress| |
| 4. CLI scaffolding, i18n and mail | 0/TBD | Not started | - | | 4. CLI scaffolding, i18n and mail | 0/TBD | Not started | - |
| 5. Data layer full fidelity | 0/TBD | Not started | - | | 5. Data layer full fidelity | 0/TBD | Not started | - |
| 6. HTTP routing, auth groups and rate limiting | 0/TBD | Not started | - | | 6. HTTP routing, auth groups and rate limiting | 0/TBD | Not started | - |

View File

@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.0 milestone: v1.0
milestone_name: milestone milestone_name: milestone
status: executing status: executing
stopped_at: Phase 3 context gathered stopped_at: Completed 03-01-PLAN.md
last_updated: "2026-09-17T16:39:57.961Z" last_updated: "2026-09-17T18:06:03.849Z"
last_activity: 2026-09-17 -- Phase 3 planning complete last_activity: 2026-09-17
progress: progress:
total_phases: 15 total_phases: 15
completed_phases: 2 completed_phases: 2
total_plans: 13 total_plans: 13
completed_plans: 9 completed_plans: 10
percent: 13 percent: 13
--- ---
@@ -21,16 +21,16 @@ progress:
See: .planning/PROJECT.md (updated 2026-09-16) See: .planning/PROJECT.md (updated 2026-09-16)
**Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test. **Core value:** An existing WinterCMS-shaped app can be ported plugin by plugin to a single Go binary without its frontend noticing: the PHP version's API contract is the acceptance test.
**Current focus:** Phase 3 — first vertical slice — genres end to end **Current focus:** Phase 03 — first-vertical-slice-genres-end-to-end
## Current Position ## Current Position
Phase: 3 Phase: 03 (first-vertical-slice-genres-end-to-end) — EXECUTING
Plan: Not started Plan: 2 of 4
Status: Ready to execute Status: Ready to execute
Last activity: 2026-09-17 -- Phase 3 planning complete Last activity: 2026-09-17
Progress: [██████████] 100% of realized plans (9/9); 2/15 roadmap phases Progress: [████████░░] 77%
## Performance Metrics ## Performance Metrics
@@ -53,6 +53,7 @@ Progress: [██████████] 100% of realized plans (9/9); 2/15 ro
- Trend: - - Trend: -
*Updated after each plan completion* *Updated after each plan completion*
| Phase 03 P03-01 | 17 min | 2 tasks | 52 files |
## Accumulated Context ## Accumulated Context
@@ -83,6 +84,9 @@ Recent decisions affecting current work:
- [Phase 02]: Fresh PHP self-replay uses disposable MariaDB fonoteka_parity_* plus process-local hex credentials, never the developer DB or caller-supplied PHP_PARITY_TARGET — T-02-01 T-02-05: check-phase2.sh --fresh-php owns the origin and rejects PHP_PARITY_TARGET - [Phase 02]: Fresh PHP self-replay uses disposable MariaDB fonoteka_parity_* plus process-local hex credentials, never the developer DB or caller-supplied PHP_PARITY_TARGET — T-02-01 T-02-05: check-phase2.sh --fresh-php owns the origin and rejects PHP_PARITY_TARGET
- [Phase 02]: Client flows run on a second winter:up after dropping tables so they are not replayed after the mutating 154-route suite — D-16 and 02-03 seed-then-clients: keep route replay and Nuxt/MCP replay on disjoint schemas - [Phase 02]: Client flows run on a second winter:up after dropping tables so they are not replayed after the mutating 154-route suite — D-16 and 02-03 seed-then-clients: keep route replay and Nuxt/MCP replay on disjoint schemas
- [Phase 02]: Capture-by-reference mismatch is a two-step share:item flow with a live token change on the second /show — D-11 D-13: contract tests exercise RecordFlow/ReplayFlow public APIs, not private helpers - [Phase 02]: Capture-by-reference mismatch is a two-step share:item flow with a live token change on the second /show — D-11 D-13: contract tests exercise RecordFlow/ReplayFlow public APIs, not private helpers
- [Phase 03]: 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 — DATA-01: one shared pool now; dual-driver River listener deferred
- [Phase 03]: Generated app main stays framework-generic (lagoon.RuntimeCommands + surf.ServeCommand); fonoteka.go/app.Handler is the in-process boot seam for parity tests — summer build cannot import the app package; CLI serve and tests still assemble the same surf router
- [Phase 03]: Empty golem15.user.jwt.secret fails Boot; tests use a fixed test-only HS256 secret and do not issue tokens through a production API — D-11: missing secret must not fall back; token minting stays out of Phase 3
### Pending Todos ### Pending Todos
@@ -104,6 +108,6 @@ Items acknowledged and carried forward from previous milestone close:
## Session Continuity ## Session Continuity
Last session: 2026-09-17T15:47:07.650Z Last session: 2026-09-17T18:04:55.976Z
Stopped at: Phase 3 context gathered Stopped at: Completed 03-01-PLAN.md
Resume file: .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md Resume file: None

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*