diff --git a/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md b/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md
new file mode 100644
index 0000000..56cf46e
--- /dev/null
+++ b/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md
@@ -0,0 +1,138 @@
+# Phase 6: HTTP routing, auth groups and rate limiting - Context
+
+**Gathered:** 2026-09-19
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+Phase 6 turns the Phase 3 routing skeleton into the full HTTP contract layer: a guard registry in `bouncer` with two real guards (JWT, personal token) resolving to one current-user accessor; the three mutually exclusive auth groups (JWT under `/_fonoteka/api/v1`, personal token under `/api/v1/fonoteka`, public/onboarding) sharing handlers; a real rate limiter behind the existing `surf.Limiter` seam with named buckets, inline throttles and stacking; response-convention helpers; structural exemption of OAuth/RFC routes from house envelope and error middleware; an SSRF-guarded outbound fetch helper; swaggo/swag OpenAPI generation with an `openapi-typescript` check; CORS and JSON body-size parity. Requirements HTTP-03 through HTTP-09. Roadmap mode: mvp. Security-load-bearing: the security-review agent runs on it.
+
+Two repos. `summercms.go` gains the limiter, guard registry, raw-group flag and route table, response helpers/types, fetch helper, path-scoped CORS and body limits — and still knows nothing about Płytarium. `fonoteka.go` gains the `inv_token` guard, `inv.scope`, the five fonoteka buckets, the group builders with PHP's middleware stacks, and genres mounted under both prefixes.
+
+Not in this phase: token CRUD and minting, register/login/refresh (Phase 7); the OAuth bearer guard and any OAuth handler (Phase 8); the bulk of the 154 routes (Phases 12–14); the manual-cover and Discogs cover fetchers and their endpoints (Phase 12/14); buckets owned by the user, feedback and websockets plugins (their own phases); HTTP DTO fuzzing (Phase 12).
+
+
+
+
+## Implementation Decisions
+
+### Rate-limit buckets and limiter
+- **D-01:** Phase 6 defines and tests fonoteka's **five** named buckets exactly as in `routes.php` — `fonoteka-api-token` (60/min, key `tok:` else client IP), `fonoteka-oauth-token` (30/min, `oauthtok:`), `fonoteka-oauth-register` (30/min, `oauthreg:`), `fonoteka-public-token` (60/min, `pubtok:`), `fonoteka-public-ip` (120/min, client IP) — plus the inline `throttle:N,M` mechanism (PHP uses `10,1`, `12,1`, `20,1`, `60,1`) and stacking of two limiters on one route (`fonoteka-public-token` + `fonoteka-public-ip`). The roadmap's "seven named buckets" is a miscount: correct the wording in ROADMAP.md and REQUIREMENTS.md HTTP-04 at plan time. Buckets of `golem15.user` (`user-api`, `pin-login`, `2fa-verify`), feedback and websockets are declared by those plugins in their own phases through the same API.
+- **D-02:** Exact Laravel `ThrottleRequests` wire parity: fixed window per key (first hit opens the window), `X-RateLimit-Limit` and `X-RateLimit-Remaining` on limited responses, `Retry-After` and `X-RateLimit-Reset` on 429, and the same 429 body PHP returns. Researcher confirms header set, body and inline-throttle key composition from the Laravel version the PHP app runs.
+- **D-03:** Counters live in-process, stdlib only: a mutex-guarded map with expiry sweep behind `surf.Limiter`, with a small Store interface so a shared backend can come later. No `otter`, no `cooler` package this phase.
+- **D-04:** Client IP comes from a trusted-proxy rule: a `http.trusted_proxies` CIDR list; `X-Forwarded-For` is honored only when `RemoteAddr` is inside that list, taking the rightmost untrusted hop; empty list means `RemoteAddr` only. One framework function is the single source of client IP for limiter keys (and later logging).
+- **D-05:** Parameterized middleware names are a `surf` feature: `throttle:10,1`, `throttle:fonoteka-public-ip` and `inv.scope:write` are written at call sites exactly as in PHP, so the routes port stays line by line (extends Phase 3 D-14).
+
+### Guard registry and personal-token guard
+- **D-06:** `bouncer` gets a named guard registry. Plugins register at Register time under a name (`jwt`, `inv_token`; `oauth` later) a Guard with an authenticate-request → principal shape. Auth middleware is derived from a guard by name. Every guard writes the same `bouncer.User(ctx)` accessor, plus an optional credential accessor (the resolved token) that `inv.scope` and the `tok:` bucket key read. Duplicate or unknown guard names fail boot.
+- **D-07:** The personal-token guard is real; only minting is test-only (same split as Phase 3 D-07). It verifies the presented token against the Phase 5 `ApiToken` model with the PHP guard's rules (hashing, expiry, revocation, last-used) and resolves the real user. Tokens are inserted by tests and parity seed hooks. Phase 7 adds CRUD and keeps this verifier.
+- **D-08:** `inv.scope:` lands in Phase 6 in the `golem15.fonoteka` plugin, as in PHP (`TokenScope.php`): it is the token group's auth step. Bodies are exact: 401 `{"error":"Invalid token"}`, 403 `{"error":"Missing required scope: "}`. The `inv_token` guard is registered by `golem15.fonoteka` too, mirroring PHP ownership.
+- **D-09:** The `oauth` guard name is reserved in docs only. No stub that pretends to validate bearer tokens ships; the registry is proven with the two real guards. Phase 8 registers `oauth`.
+- **D-10:** The Phase 3 JWT verifier, its 401 bodies and the 423 gate are kept unchanged; `jwt.auth` is re-expressed as the `jwt` guard in the registry without changing behavior.
+
+### Outbound fetch helper
+- **D-11:** One framework helper with a per-call Policy offering both PHP modes: `AllowHosts` (exact and dotted-suffix match, so `evil-discogs.com` never passes) and `PublicOnly` (any host, every resolved IP must be public). The private/loopback/reserved IP block is always on, including under an allow-list. https only, redirects disabled, byte cap enforced while streaming, overall timeout. Roadmap criterion 4 is tested through the allow-list mode; manual cover URL keeps PHP's any-public-host behavior.
+- **D-12:** The IP check happens at dial time (`net.Dialer` Control hook on the actual address being connected), closing the resolve-then-connect rebinding gap PHP's comments admit. Stdlib only. This is invisible on the wire and not a contract change.
+- **D-13:** Phase 6 ships the helper and its tests only (against `httptest` servers), with typed failure reasons that map onto PHP's `invalid_url` / `unresolvable` / `private_ip` / `network_error` / `too_large` family. `ManualCoverUrlFetcher` and `CoverImporter` ports are Phase 12/14 call sites.
+- **D-14:** Byte cap and timeout have framework defaults in config (`http.fetch.*`, defaulting to PHP's 10 MiB and 10 s), overridable per call. A call can lower or raise them but never reach "unlimited": zero or negative effective values are an error.
+
+### Route surface, OAuth exemption, conventions, CORS
+- **D-15:** All seven PHP route groups exist in `fonoteka.go` as group builders with their exact middleware stacks, but only routes with real handlers are mounted. The shared-handler proof is `GET genres` under both `/_fonoteka/api/v1` (JWT) and `/api/v1/fonoteka` (`inv.scope:read`), which flips `GET__api_v1_fonoteka_genres_personal_token` to ported. Public-group and stacked-limiter behavior is proven on a real route if one is cheaply portable, otherwise on framework fixture routes in tests. No 501 shells; manifest entries stay `pending` until a real handler exists (pending never equals passing).
+- **D-16:** OAuth/RFC exemption is a group flag enforced at registration: a raw group refuses any middleware tagged as house envelope/error and fails boot if one is named; recover on a raw group emits a bare 500 with no house JSON body. The router exposes a route table (method, pattern, plugin, middleware chain, raw flag) used by tests and a `route:list` command; success criterion 3's "verified by route-registration inspection" is a test over that table. The `/.well-known/oauth-authorization-server` and `/oauth/mcp/*` group is declared raw now with its two throttles, handlers arrive in Phase 8.
+- **D-17:** Response conventions are framework helpers and types, not middleware: a JSON writer plus small types (Carbon-format `+00:00` time, nullable tri-state bool, never-nil slice helper); handlers build DTOs explicitly and omit conditional keys. Nothing wraps or rewrites responses after the handler. Tested at type level and on the genres routes.
+- **D-18:** CORS becomes path-scoped and config-driven like Laravel (`paths`, origins, methods, headers, `max_age`, credentials). Fonoteka's config reproduces `config/cors.php` exactly — including that `_fonoteka/api/*` is **not** listed, so the JWT group gets no CORS headers, as today. JSON body limits use `http.MaxBytesReader` per group with a larger cap for upload routes; the researcher reads the real PHP/nginx deployment values rather than guessing.
+
+### Claude's Discretion
+- Package homes and names (where the fetch helper, client-IP function and response types live), limiter sweep interval, Store interface shape.
+- Inline `throttle:N,M` key composition, as long as it matches Laravel's (D-02 research).
+- swag workflow: which repo holds annotations and generated spec, the `summer` command or script that runs swag and `openapi-typescript`, and a drift check. Default: annotations on the real fonoteka handlers, generated spec committed in `fonoteka.go`, types validity checked in the phase gate script.
+- Locale stage depth beyond Phase 3's `Accept-Language` read, only as far as the ported routes need.
+- How mutual exclusivity of groups is asserted (route-table test preferred).
+- Plan count and split, subject to the plan-count checkpoint and "unit tests are the last plan"; security-review agent included.
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### The PHP contract
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/routes.php` — lines 37–53 and 390–396 (the five `RateLimiter::for` buckets and the reasoning comments), 61–80 (JWT groups), 351–364 (onboarding/invitation public group, `throttle:10,1`), 399–426 (public share groups, `PublicShareHeaders`, stacked limiters), 449–514 (token group, `inv.scope`, genres at 513), 547–565 (OAuth group and its throttles), and every inline `throttle:` call site.
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/middleware/TokenScope.php` — 401/403 bodies and guard usage (D-08).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/Plugin.php` around lines 121 and 262, and `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/auth/` — the `inv_token` guard registration and implementation (D-07).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/middleware/PublicShareHeaders.php` — headers on public share routes.
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/classes/ManualCoverUrlFetcher.php` and `classes/discogs/CoverImporter.php` — the two SSRF policies, failure reasons, 10 MiB / 10 s defaults, no redirects (D-11–D-14).
+- `/media/nvme/dev/golem15/fonoteka/config/cors.php` — exact CORS parity target (D-18); `/media/nvme/dev/golem15/fonoteka/config/cache.php` — file cache driver behind PHP's limiter.
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/user/middleware/JwtAuthenticate.php` — unchanged JWT contract (D-10).
+- `/media/nvme/dev/golem15/fonoteka/plugins/golem15/fonoteka/traits/SerializesFonoteka.php` — source of the response conventions (D-17).
+
+### Parity harness
+- `../fonoteka.go/parity/fixtures/routes/GET__api_v1_fonoteka_genres_personal_token.yaml`, `GET__api_v1_fonoteka_me_personal_token.yaml`, `GET___fonoteka_api_v1_public_{token}_*.yaml`, `GET___fonoteka_api_v1_invitations_{token}_public_invitation.yaml` — fixtures for the token and public groups.
+- `../fonoteka.go/parity/manifest.yaml`, `../fonoteka.go/parity/parity_test.go` — pending/ported status and seed hooks (token insertion for D-07).
+
+### Existing framework code
+- `surf/router.go` — group builder, named middleware, `Limiter`/`noopLimiter` seam, `cors`, `recoverJSON`, `Assemble`; `surf/params.go` — 404 rule.
+- `bouncer/jwt.go`, `bouncer/context.go` — verifier, `Principal`, `WithUser`/`User`.
+- `../fonoteka.go/plugins/golem15/fonoteka/models/api_token.go` — Phase 5 model the guard reads.
+
+### Research and prior decisions
+- `.planning/research/PITFALLS.md` §Rate-limit bucket parity, §Pitfall 4 (nil slice), §Pitfall 8 (envelopes and error shapes), §Security Mistakes.
+- `.planning/research/ARCHITECTURE.md` §Request Flow (auth groups; note `cooler`/otter is superseded by D-03 for this phase).
+- `.planning/research/STACK.md` §OpenAPI generation (swaggo/swag v1.16.6, openapi-typescript).
+- `.planning/phases/03-first-vertical-slice-genres-end-to-end/03-CONTEXT.md` D-07, D-09, D-10, D-12, D-14, D-15 — what Phase 3 left as seams for this phase.
+- `.planning/REQUIREMENTS.md` HTTP-03–HTTP-09; `.planning/ROADMAP.md` Phase 6; `CLAUDE.md` GSD lean-mode rules.
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `surf.Router`/`Group` with `Use`, `Where`, `WhereIn`, fail-boot on unknown middleware names; only `Get` exists today — other verbs are needed for later phases but only as far as mounted routes require.
+- `surf.Limiter` interface with `noopLimiter` in the fixed pipeline slot: the real limiter drops in here.
+- `bouncer.Middleware`, `Verify`, `Principal`, `User(ctx)`: the JWT guard to wrap into the registry.
+- `ApiToken`, `OAuthClient`, `OAuthAuthCode`, `OAuthRefreshToken` models (Phase 5).
+- Parity `seedHooks` seam and the genres JWT route already ported and green.
+
+### Established Patterns
+- Stdlib first; new dependency this phase is only `swaggo/swag` (STACK-named). Limiter, fetch guard and CORS are stdlib.
+- Request state in `context.Context` with unexported keys and exported accessors; fail boot on misconfiguration.
+- Verifier real / minting test-only split for security-sensitive guards.
+- Framework never imports the app; PHP ownership decides which plugin registers a middleware or guard.
+
+### Integration Points
+- `surf.Assemble` — CORS becomes path-scoped, limiter becomes real, raw-group handling and the route table are added here.
+- `bonfire` kernel — `route:list` command.
+- `golem15.fonoteka` `plugin.go`/routes — group builders, guard and bucket registration, genres under the token prefix.
+- `compass` config — `http.trusted_proxies`, `http.cors.*`, `http.fetch.*`, body limits.
+
+
+
+
+## Specific Ideas
+
+- Limits and keys are contract, not tuning: the `fonoteka-oauth-*` 30/min-per-IP values are deliberately generous because DCR traffic comes from vendor egress IPs.
+- Route files should still read like `routes.php`, including `throttle:10,1` and `inv.scope:write` strings.
+- Go may be safer than PHP where the wire contract cannot tell (dial-time SSRF check, trusted-proxy IP), never looser.
+- OAuth exemption must be enforced by the router and provable from the route table, not upheld by convention.
+
+
+
+
+## Deferred Ideas
+
+- `user-api`, `pin-login`, `2fa-verify` buckets — Phase 7 with the user plugin; feedback and websockets buckets with their plugin ports.
+- Shared/Postgres-backed limiter store for multi-replica deployments — after v1; the Store interface keeps it possible.
+- `cooler` cache package on otter — when a phase actually needs an in-process cache.
+- `ManualCoverUrlFetcher` and `CoverImporter` ports with MIME handling — Phase 12/14.
+- OAuth bearer guard and OAuth handlers on the raw group — Phase 8.
+- Token CRUD and production minting — Phase 7.
+
+
+
+---
+
+*Phase: 06-http-routing-auth-groups-and-rate-limiting*
+*Context gathered: 2026-09-19*
diff --git a/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-DISCUSSION-LOG.md b/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-DISCUSSION-LOG.md
new file mode 100644
index 0000000..efe2384
--- /dev/null
+++ b/.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-DISCUSSION-LOG.md
@@ -0,0 +1,157 @@
+# Phase 6: HTTP routing, auth groups and rate limiting - Discussion Log
+
+> **Audit trail only.** Do not use as input to planning, research, or execution agents.
+> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
+
+**Date:** 2026-09-19
+**Phase:** 6-HTTP routing, auth groups and rate limiting
+**Areas discussed:** Bucket scope & limiter, Token guard depth, Outbound fetch guard, Route surface & OAuth exempt
+
+---
+
+## Bucket scope & limiter
+
+### Which buckets does Phase 6 define and test?
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Fonoteka 5 + mechanism | Five fonoteka named buckets plus inline throttle form; other plugins' buckets land with those plugins; roadmap "seven" corrected | ✓ |
+| All 11 now | Every named bucket of every ported plugin declared in Phase 6 | |
+| Fonoteka 5 + user 3 | Add the user stub's three buckets now | |
+
+### Wire behavior
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Exact Laravel parity | Fixed window, X-RateLimit-* headers, Retry-After/Reset on 429, same 429 body | ✓ |
+| Same limits, Go-native algorithm | Token bucket/sliding window, same headers, edge bursts may differ | |
+| You decide | Researcher picks cheapest faithful option | |
+
+### Counter store
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| In-process, stdlib | Mutex map + sweep behind surf.Limiter, Store interface for later | ✓ |
+| In-process via otter (cooler) | Adds otter and the cooler package now | |
+| Postgres-backed | Survives restarts, DB write per limited request | |
+
+### Client IP
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Trusted-proxy config | XFF honored only from trusted CIDRs, rightmost untrusted hop | ✓ |
+| RemoteAddr only | Ignore forwarding headers | |
+| Always trust X-Forwarded-For | Spoofable if directly reachable | |
+
+**Notes:** Scouting found five named buckets in fonoteka/routes.php, not seven; eight more live in user, feedback, websockets and journal plugins.
+
+---
+
+## Token guard depth
+
+### How real is the personal-token guard?
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Real verifier, test-only minting | ApiToken lookup with PHP rules; tokens inserted by tests/seed hooks; Phase 7 adds CRUD | ✓ |
+| Interface + fake guard | Real lookup waits for AUTH-03 | |
+
+### Where does inv.scope land?
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Phase 6, in golem15.fonoteka | It is the token group's auth step; exact 401/403 bodies; parameterized names in surf | ✓ |
+| Phase 7 with AUTH-03 | Guard-only auth in Phase 6 | |
+
+### Guard registry shape
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Named guards in bouncer | Registry by name, Guard interface, single user accessor plus credential accessor, fail boot on duplicates | ✓ |
+| Just named middleware | Convention only | |
+| You decide | Planner picks | |
+
+### OAuth bearer guard slot
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Name reserved, no implementation | Registry proven with two real guards | ✓ |
+| Stub that always 401s | Mount /oauth/mcp behind a rejecting guard | |
+
+---
+
+## Outbound fetch guard
+
+### Host policy
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Per-caller policy, both modes | AllowHosts and/or PublicOnly; private-IP block always on | ✓ |
+| Allow-list only | Manual cover URL would need an allow-list (behavior change) | |
+
+### DNS rebinding
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Dial-time IP check | net.Dialer Control validates the connected IP | ✓ |
+| Match PHP: pre-resolve only | Keep the gap, note the risk | |
+
+### Scope
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Framework helper + tests only | Fetcher ports are Phase 12/14 | ✓ |
+| Helper + both fonoteka fetchers | Port ManualCoverUrlFetcher and CoverImporter now | |
+
+### Limits source
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Caller passes them; no framework default-open | Zero value fails construction | |
+| Framework defaults in config | http.fetch.* defaults, overridable per call | ✓ |
+
+**Notes:** User chose framework config defaults over the recommended caller-required values. Recorded with the constraint that an effective limit can never be unlimited.
+
+---
+
+## Route surface & OAuth exempt
+
+### Route surface
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Groups + proving routes only | All groups as builders; only real handlers mounted; genres under both prefixes; no 501 shells | ✓ |
+| All 154 as 501 shells | Whole surface registered now | |
+| Shells for auth-sensitive routes only | Public, token and OAuth groups fully registered | |
+
+### OAuth exemption
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Group flag + boot-time inspection | Raw group refuses house middleware, bare 500 on recover, route table + route:list | ✓ |
+| Separate mux mounted beside the pipeline | Own ServeMux and minimal chain | |
+| You decide | Planner picks | |
+
+### Response conventions
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Framework helpers + types, no envelope middleware | JSON writer, Carbon time, nullable bool, never-nil slice | ✓ |
+| Helpers live in fonoteka.go | Framework stays neutral | |
+
+### CORS and body limits
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Path-scoped CORS from config, exact parity | Reproduce cors.php, no CORS on /_fonoteka/api; body limits from real deployment values | ✓ |
+| CORS on everything | Header difference on the JWT group | |
+
+---
+
+## Claude's Discretion
+
+Package homes and names, sweep interval and Store shape, inline throttle key composition (must match Laravel), swag/openapi-typescript workflow and drift check, locale stage depth, how group mutual exclusivity is asserted, plan count and split.
+
+## Deferred Ideas
+
+User/feedback/websockets buckets with their plugins; shared limiter store after v1; cooler/otter cache when needed; cover fetcher ports in Phase 12/14; OAuth guard and handlers in Phase 8; token CRUD in Phase 7.