D-07 D-08 D-09 D-10 D-11: a valid HS256 token for a persisted user reaches the route; missing, malformed, expired, wrong-algorithm, bad-signature and unknown-user tokens do not.
D-12 D-13 D-14: golem15.fonoteka requires golem15.user and mounts GET /_fonoteka/api/v1/genres through the named middleware pipeline, including jwt.auth and inv.must-change-password.
D-16 D-17 D-18 D-19: a fresh Polish-ICU Postgres database migrates the user and Fonoteka sets in dependency order and contains the 15 canonical genres without a seeder command or AutoMigrate.
DATA-01: GORM and application services use the same pgx-stdlib *sql.DB; the River listener pool is a documented Phase 11 seam.
path
provides
lagoon/connection.go
One checked pgx stdlib SQL pool passed to GORM
path
provides
lagoon/migrations.go
Per-plugin gormigrate sets and version tables
path
provides
surf/router.go
Group declarations compiled to net/http ServeMux
path
provides
bouncer/jwt.go
Pinned HS256 verifier and request identity
path
provides
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
First real app route mounted through the user plugin
path
provides
../fonoteka.go/app/app.go
Reusable app boot seam shared by generated main and parity tests
from
to
via
party/registry.go
pact/capabilities.go
optional capability discovery in Requires order
from
to
via
lagoon/connection.go
backpack/app.go
app-scoped *sql.DB and *gorm.DB publication
from
to
via
../fonoteka.go/plugins/golem15/fonoteka/plugin.go
../fonoteka.go/plugins/golem15/user/plugin.go
Requires and named jwt.auth lookup
**As a** Fonoteka user with a valid token, **I want to** request the seeded genre list from a real Postgres backed Go app, **so that** the first complete config → plugin → DB → auth → HTTP path exists.
Purpose: Establish a runnable vertical slice on the actual app binary. Plan 02 refines the query and Plan 03 makes the recorded PHP parity fixture green.
Output: Shared database and migration runtime, ServeMux group and middleware registry, HS256 verifier, two app plugins, and a JWT protected genre route.
@.planning/ROADMAP.md
@.planning/REQUIREMENTS.md
@.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md
@.planning/phases/03-first-vertical-slice-genres-end-to-end/03-RESEARCH.md
@.planning/phases/03-first-vertical-slice-genres-end-to-end/03-VALIDATION.md
@.planning/phases/03-first-vertical-slice-genres-end-to-end/03-PATTERNS.md
`party.Plugin` has `ID`, `Requires`, `Register(*backpack.App) error`, `Boot(*backpack.App) error`; `party.Activate` returns dependency ordered plugins. `pact.HasConfig` and `HasCommands` are current optional capability patterns. `backpack.App` publishes app scoped services. `bonfire.Command` uses string flags and `Run(context.Context, bonfire.Input, bonfire.Output) error`; `validCommandName` currently permits only `build` and `dev` as bare names. `internal/build/build.go` generates app `main.go` and blank imports. The app parity suite already has a pgx `*sql.DB` from testcontainers.
Task 1: Initialize the real app database through plugin migrations
go.mod, go.sum, bonfire/command.go, internal/build/build.go, pact/capabilities.go, party/registry.go, lagoon/connection.go, lagoon/migrations.go, lagoon/commands.go, cmd/summer/main.go, ../fonoteka.go/go.mod, ../fonoteka.go/go.work, ../fonoteka.go/README.md, ../fonoteka.go/summer.yaml, ../fonoteka.go/config/app.yaml, ../fonoteka.go/parity/synthetic_test.go, ../fonoteka.go/main.go, ../fonoteka.go/plugins.gen.go, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/user/config/config.yaml, ../fonoteka.go/plugins/golem15/user/migrations.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/migrations.go, ../fonoteka.go/plugins/golem15/fonoteka/genre.go
go.mod, bonfire/command.go, cmd/summer/main.go, internal/build/build.go, pact/capabilities.go, party/registry.go, backpack/app.go, compass/config.go, compass/env.go, ../fonoteka.go/go.mod, ../fonoteka.go/README.md, ../fonoteka.go/parity/synthetic_test.go, examples/hello/main.go, examples/hello/summer.yaml, examples/hello/plugins/base/config/config.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/version.yaml, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.0.9/rename_taxonomy_and_notes_columns.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/updates/v1.1.0/seed_genre_and_various_artist_taxonomy.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/models/Collection.php, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-RESEARCH.md
Build the smallest useful command path: `fonoteka migrate` and `summer migrate` in an app directory use the same `bonfire` command contract to load config, activate generated plugin imports, open `database.dsn` (env `SUMMER_DATABASE__DSN`) through `sql.Open("pgx", ...)`, `PingContext`, publish that exact `*sql.DB` and a GORM handle opened with `postgres.Config{Conn: sqlDB}`, then run each plugin's ordered `[]*gormigrate.Migration` from `pact.HasMigrations` in `party.Activate` order. Extend `bonfire.validCommandName` for bare `serve` and `migrate`; keep colon names for `migrate:rollback` and `migrate:status`. The `summer` tool may delegate app runtime commands to the generated binary, but cannot import app packages or recurse. Give each plugin a validated, separate history table (`summer_migrations_golem15_user`, `summer_migrations_golem15_fonoteka`) with transactions. Create the app workspace and two modules; `golem15.fonoteka.Requires()` returns `[]string{"golem15.user"}`. The user set creates minimal `users` with `must_change_password`; its config file declares `jwt.secret` under the merged `golem15.user.jwt.secret` path (env `SUMMER_GOLEM15__USER__JWT__SECRET`) without a production default. The Fonoteka set creates full `golem15_fonoteka_genres` directly, with no `item_categories` detour, plus minimal collections/albums/editor pivot/context tables required by D-01–D-03, then seeds the 15 slug/name pairs in PHP order in a separate idempotent data migration with a reverse callback. Name the PHP update files in migration comments. Use explicit DDL, never `AutoMigrate`. Do not create a River listener pool. Document `CREATE DATABASE ... TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'` in app README/config setup, and make boot check Postgres 16 `pg_database.datlocprovider='i'` and `daticulocale='pl-PL'` before migration. Set the existing Postgres 16 testcontainer's `POSTGRES_INITDB_ARGS` to `--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8` via `testcontainers.WithEnv`, so both synthetic and real app tests use the same database default. Resolve only the modules named in RESEARCH.md; inspect `go.mod`/`go.sum` and official upstream paths because slopcheck's age heuristic flagged recent GORM/gormigrate versions.
go vet ./... && go test ./... && (cd ../fonoteka.go && go vet ./... && go test ./...)
- A fresh Postgres 16 database with ICU `pl-PL` accepts `fonoteka migrate`, yields exactly 15 genre slugs in PHP seed order, and has separate user/Fonoteka history tables.
- A libc or non-Polish database fails before migrations with an actionable locale error; no migration calls `AutoMigrate`.
- The same `*sql.DB` is supplied to GORM and app services; the separate River listener pool is deferred to Phase 11.
- Both root and app `go vet ./...` and `go test ./...` exit 0 before commit.
A developer can initialize the actual Fonoteka Go database with one command and immediately query the 15 migrated genres.
Task 2: Serve the seeded genre list behind real cross-plugin JWT middleware
pact/capabilities.go, party/registry.go, surf/router.go, surf/middleware.go, bouncer/jwt.go, bouncer/context.go, lagoon/commands.go, internal/build/build.go, ../fonoteka.go/app/app.go, ../fonoteka.go/parity/genre_smoke_test.go, ../fonoteka.go/main.go, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/genre.go
pact/capabilities.go, party/registry.go, bonfire/command.go, internal/build/build.go, backpack/app.go, towel/context.go, ../fonoteka.go/main.go, ../fonoteka.go/parity/synthetic_test.go, ../fonoteka.go/parity/parity_test.go, ../fonoteka.go/plugins/golem15/user/plugin.go, ../fonoteka.go/plugins/golem15/fonoteka/plugin.go, /media/nvme/dev/golem15/fonoteka/plugins/golem15/user/middleware/JwtAuthenticate.php, /media/nvme/dev/golem15/fonoteka/vendor/php-open-source-saver/jwt-auth/src/Http/Middleware/BaseMiddleware.php, /media/nvme/dev/golem15/fonoteka/vendor/php-open-source-saver/jwt-auth/src/Claims/Expiration.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/middleware/RequirePasswordChange.php, /media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php, /media/nvme/dev/golem15/fonoteka/config/jwt.php, ../fonoteka.go/parity/fixtures/routes/get_genres_jwt.yaml, .planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md
Add `pact.HasRoutes`, `HasMiddleware`, and `HasModels` as optional interfaces, then assemble a per-app `surf` router on `http.NewServeMux` after all plugins register. Implement `Group(prefix, surf.Use(names...), callback)`, `Get(path, http.HandlerFunc)`, and per-route named middleware; compile the genre declaration as `GET /_fonoteka/api/v1/genres`. Resolve middleware names at boot, with a missing name error containing the registering plugin ID and name. Wrap every route in recover → CORS → locale → auth group → must-change-password → org context → rate limit → handler; recover returns opaque JSON 500, CORS preflight uses configured `http.cors.allowed_origins` and bypasses auth, locale stores `Accept-Language` in context, org context is a slot, rate limit is a no-op interface. In `bouncer`, parse JWT with `WithValidMethods([]string{"HS256"})` and `WithExpirationRequired`, require nonempty `sub`, load the corresponding persisted user through a `UserProvider`, and store it through an unexported context key. `golem15.user` registers `jwt.auth`, reads a nonempty secret from `golem15.user.jwt.secret` with `SUMMER_GOLEM15__USER__JWT__SECRET` overlay, and reproduces PHP's 401 JSON `{"error":true,"message":...}`. Use `Token not provided` for a missing bearer, `Token has expired` for an expired token, and `User not found` for an unknown subject as named in the PHP JWT package; capture the PHP response for malformed and bad-signature cases on the isolated Phase 2 PHP server before finalizing those translations. `golem15.fonoteka` registers `inv.must-change-password`, returns `{"error":"Password change required","must_change_password":true}` with 423, and references both names in its group. The initial handler reads all migrated genres through GORM and emits a dedicated `data` DTO with `id`, `name`, `slug`, integer `album_count:0`, an initialized array, `Content-Type: application/json` and `Cache-Control: no-cache, private`; Plan 02 replaces the zero count with the complete scoped query. Expose a graceful signal-aware `serve` command on both binaries. Put the app boot and handler assembly behind an importable `../fonoteka.go/app` package that accepts config, plugin IDs and the existing SQL pool; generated main and parity tests call the same seam, with compiled plugin imports kept in the appropriate entry point. Add a focused `genre_smoke_test.go` in the existing Postgres test package that boots this seam and sends one valid-token HTTP request without marking corpus routes ported yet. Use a fixed test-only token secret; token issuing remains outside production APIs.
go vet ./... && go test ./... && (cd ../fonoteka.go && go vet ./... && go test ./...)
- A valid HS256 token for a persisted user returns 200 from `GET /_fonoteka/api/v1/genres` and a nonempty `data` array read from Postgres.
- Missing, malformed, expired, wrong-algorithm, bad-signature, missing-subject and unknown-user tokens never reach the handler; a locked user receives PHP's 423 JSON.
- Missing `jwt.auth` or `inv.must-change-password` fails boot with the plugin and middleware name; the framework imports no Fonoteka package.
- Both binaries expose `serve`; `summer build` regenerates the app entry point without losing runtime commands; root and app vet/test exit 0.
A token-bearing user can fetch the real seeded genre list through the full named middleware path.
<threat_model>
Trust Boundaries
Boundary
Description
Config and Go module resolution → process
Secret, DSN and dependency metadata enter the runtime.
Bearer token → user lookup → handler
Untrusted claims cross into authenticated request context.
Plugin declarations → ServeMux
A missing or misordered named guard could expose a route.
STRIDE Threat Register
Threat ID
Category
Severity
Component
Disposition
Mitigation Plan
T-03-01
Tampering
high
migrations/locale
mitigate
Explicit immutable DDL, per-plugin history tables, database locale check before migrations.
Official upstream path verification plus go.mod/go.sum review; document slopcheck's recent-version false positives.
</threat_model>
Demonstrate database migration and one authenticated HTTP request on Postgres 16 ICU `pl-PL`. Run root and app vet/test after each task; preserve Phase 2 parity tests even though the route stays pending until Plan 03.
<success_criteria>
The app binary migrates and serves the 15 seeded genres from Postgres through a real JWT guard and cross-plugin named middleware, using the same SQL pool as GORM and no AutoMigrate.
</success_criteria>
Create `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-01-SUMMARY.md` after completion.