diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md
new file mode 100644
index 00000000..8e53ba76
--- /dev/null
+++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-CONTEXT.md
@@ -0,0 +1,146 @@
+# Phase 19.4: OIDC provider on wristband - Context
+
+**Gathered:** 2026-10-17
+**Status:** Ready for planning
+
+
+## Phase Boundary
+
+An application on SummerCMS can be an OpenID Connect provider that the relying parties of the PHP `Golem15.OAuthServer` plugin ("Login with …") switch to without noticing: discovery, JWKS with asymmetric signing and key rotation, `id_token`, userinfo, resumable authorization with account switching, a consent store, public and pairwise subjects, the user's authorized-apps list and revoke, and a scheduled prune. The existing issuer, signing keys, client secrets and the six PHP tables carry over.
+
+The work splits in two (D-01):
+
+- **`summercms.go` / `wristband`**: the OIDC protocol on top of the existing MCP authorization engine. Discovery, JWKS, `id_token`, userinfo, nonce, subjects, consent and key interfaces. It stays a protocol leaf with no ORM and no application names.
+- **New shared plugin `sm-oauthserver-plugin`** (`git.golem15.com/golem15/sm-oauthserver-plugin`, plugin ID `golem15.oauthserver`): the GORM stores for both MCP and OIDC, the six tables plus a keys table, the Winter import, the cabana Clients screen, branding settings, consent and error pages, `oauthserver:rotate-keys`, `oauthserver:prune` and the routes at the PHP paths. It requires `sm-user-plugin`.
+
+Out of scope:
+- WavePath's own app plugin and its frontend. The WavePath port milestone installs this plugin.
+- Social login (Phase 19.5), 2FA and device flow (19.6), and the generic MCP module (19.7). 19.7 can later enable `openid` on the MCP server (D-07).
+- Moving fonoteka's own `wristband.Backend` adapter onto the plugin's stores (deferred).
+
+**Roadmap corrections** (make them in the first planning commit):
+- "Repos" line: add `sm-oauthserver-plugin` (new) next to `summercms.go`.
+- SC-1: the decision note also names the JOSE/JWT dependency, if research picks the hand-written path (D-16).
+
+
+
+
+## Implementation Decisions
+
+### Where the pieces live
+- **D-01:** The protocol lives in `wristband`. Storage, UI, import and commands go in a new shared `sm-oauthserver-plugin`, which mirrors PHP `golem15/oauthserver`. The point is one reviewed authorize/token engine for both MCP and OIDC, which the framework-level MCP module (19.7) can also use. Without the plugin wristband serves nothing on its own, and that is accepted. — **Reversibility:** costly — moving the protocol later touches both repos and fonoteka.
+- **D-02:** The plugin's stores serve **both** flows: MCP clients (created by dynamic registration) and OIDC clients (registered by an admin). An app that wants MCP sign-in installs the plugin instead of writing its own adapter. Fonoteka keeps its own adapter in this phase.
+- **D-03:** Consent and error pages: by default the plugin renders HTML on the server, like PHP's `consent.htm` and `error.htm`, with the branding settings (site name, logo, colours, font CSS URL and family, custom CSS) and a one-shot CSRF token on the consent POST. A config option redirects to the application's own consent page instead (a JSON API in the style of wristband's existing `/connect?request=` flow).
+- **D-04:** Login handoff stays as in PHP. An anonymous user goes to the configured login URL (config key mirroring `OAUTHSERVER_LOGIN_URL`) with a single `?redirect=/oauth/authorize/resume/{request}`. The OIDC query string never travels through the login page. The signed-in user is read from bouncer's `token` JWT cookie, whose attributes match PHP since 19.3. The switch-account flow matches PHP's `/oauth/authorize/switch-account`.
+- **D-05:** The plugin **requires `sm-user-plugin`**: claims (name, email, avatar) come from its users, the subject and consent cascade hangs off its user-delete event, and authorizations routes use its JWT guard. No claims-provider interface. Changes to `sm-user-plugin` are additive only (core-plugin rule), and both of its consuming apps must still build.
+
+### MCP and OIDC together
+- **D-06:** **Two `wristband.Server` values, one engine.** The app builds one Server for MCP (`/oauth/mcp/*`, RFC 8414 metadata, DCR, `read`/`write`/`ai`/`offline_access`) and one for OIDC (`/.well-known/openid-configuration`, `/oauth/authorize`, `/oauth/token`, `/oauth/userinfo`, `/oauth/jwks`, `openid`/`profile`/`email`). Both share the Backend and the issuer, and each mounts its own routes and metadata. Fonoteka's existing single MCP server compiles and behaves unchanged.
+- **D-07:** MCP clients **cannot** request `openid` or receive an `id_token` in 19.4. The engine supports it, so 19.7 may switch it on.
+- **D-08:** Each client row has a **kind** (`mcp` | `oidc`). Each Server accepts only its own kind at authorize, token and userinfo, so dynamic registration can never produce a client that gets OIDC identity scopes.
+- **D-09:** **Additive interfaces.** OIDC storage needs (consents, subjects, nonce on codes and requests, signing keys, the resume/switch-account state on pending requests) arrive as new, separate interfaces. They are required only when a Server enables OIDC and are checked at construction or `SetBackend`. Existing `wristband.Backend`/`wristband.Tx` and fonoteka's adapter stay untouched, and a framework minor version bump is enough. — **Reversibility:** costly — the published wristband API is consumed by fonoteka.
+
+### Signing keys
+- **D-10:** Keys live in the **database**: a keys table with kid, algorithm, the public key in plain text, the private PEM encrypted with the app key (`lagoon.Encrypted` or equivalent), created/retired timestamps and an active marker. Every instance sees the same ring, which is what 19.3's multi-instance setup needs. As in PHP, a first key is generated lazily if none exists. Private key material is never logged and never leaves the signing path.
+- **D-11:** The Winter import reads PHP's `key_dir` (`keys.json` + `.key`/`.pub`) and inserts every key with its **original kid** and retired state, so relying parties' cached JWKS and tokens already in flight keep verifying. — **Reversibility:** one-way — a cutover with new kids breaks every relying party's verification until it refetches.
+- **D-12:** **RS256 by default** (imported PHP keys are RS256 2048). **ES256 is configurable** for new keys, and the JWKS may hold both kinds during a switch. Discovery advertises what the ring holds.
+- **D-13:** Rotation: an `oauthserver:rotate-keys` command as in PHP. The retired key stays in the JWKS for a grace window (default 24h, config, mirroring `rotation_grace_hours`), and the prune job removes it afterwards. An opt-in scheduled rotation (interval in config, off by default) uses the framework scheduler.
+
+### Parity bar
+- **D-14:** Parity means the **relying-party-visible contract**, recorded from PHP with `tide` and replayed against Go: the discovery document, JWKS shape, token and userinfo JSON, error redirects and error bodies, the `id_token` header (`alg`, `kid`) and claim set (including `nonce`, `at_hash`, `auth_time`/`azp` if PHP emits them), the authorize/resume/decide redirects, and the authorizations list/revoke JSON. The PHP test suite (`plugins/golem15/oauthserver/tests/`) is mapped case by case. Consent and error HTML must work the same and honour branding, but need not be byte-identical.
+- **D-15:** Subjects: **public by default**. Imported PHP subs are random UUIDv4 values that never rotate, one per user, allocated lazily on first authorize and hard-deleted with the user (PHP D-98-01..04). A client can opt into **`subject_type=pairwise`** (sector identifier derived from its redirect host, or a configured sector identifier URI). Discovery advertises `public` and `pairwise`. This goes beyond PHP (D-98-02 chose public only) because the roadmap asks for it.
+- **D-16:** The library question is **left to research**. The decision note (SC-1) compares extending the hand-written wristband (plus a JOSE/JWK library for RS256/ES256 and JWKS) against putting zitadel/oidc v3 behind it. Whichever is chosen must keep D-06 (one engine for MCP and OIDC) and D-09 (additive interfaces, fonoteka unchanged). An interop test with a real RP library (the equivalent of PHP's `JumbojettInteropTest`) is required either way, and the RP library may be test-only.
+- **D-17:** Refresh tokens for OIDC are **opt-in per client**. An OIDC client allowed `offline_access` gets wristband's existing rotating refresh tokens with lineage revocation. Other clients get only the code grant, as in PHP (D-98-14). Imported PHP clients default to no `offline_access`.
+- **D-18:** Throttles are ported with **the same buckets and keys** as named surf limiters on the 19.3 Redis-backed store: authorize 30/min per user, falling back to IP; token 20/min per `client_id`, falling back to IP; userinfo 60/min per SHA-256 of the bearer token, falling back to IP; authorizations 120/min per user, falling back to IP. Discovery and JWKS stay unthrottled and cacheable.
+
+### Import and secrets
+- **D-19:** An import command in the plugin (pattern of `pgstripe:import-winter` / the D-02 runner) copies the six PHP tables: clients, auth_codes, access_tokens, auth_requests, consents and subjects. PHP hashes client secrets, codes and access tokens as SHA-256 hex, the same scheme as wristband's `sha256Hex`, so they are copied as-is and live access tokens keep working. The researcher confirms byte-for-byte compatibility. The issuer is set from config to the PHP `OAUTHSERVER_ISSUER` value.
+
+### Claude's Discretion
+- Table names (keep `golem15_oauthserver_*` so the import is a straight copy unless there is a reason not to), the exact Options/interface names, the cabana screens' layout (stock fields, like PHP's "stock widgets only"), how the consent-page redirect option is spelled in config, and the scheduled-rotation default interval.
+
+### Folded Todos
+- **Neutral default resource URL in `wristband.DefaultOptions`** (`.planning/todos/pending/wristband-neutral-resource-default.md`, g15office `iarcg5dk`): `DefaultOptions().Resource` and comments in `server.go`, `stores.go` and `client_issue.go` name a consuming application. 19.4 reworks wristband's Options anyway, so the default becomes empty or neutral, and the comments say "the host application". Check how authorize treats an empty `Resource` first, and set `Resource` explicitly in fonoteka (a behaviour change for the host, done in the same release).
+
+
+
+
+## Canonical References
+
+**Downstream agents MUST read these before planning or implementing.**
+
+### The PHP original (port source and parity target)
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/routes.php`: complete route surface, middleware groups, throttle buckets, route-shadow note against the user plugin's `/oauth/{provider}`
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/controllers/oidc/`: Authorize (start/resume/decide/switchAccount), Token, UserInfo, Jwks, Discovery, Authorizations
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/classes/`: KeyRing (rotation, grace, on-disk layout), TokenIssuer (opaque access token, RS256 id_token, `at_hash`), ClaimBuilder, ScopeCatalog, RedirectUriMatcher, PkceVerifier, ConsentCsrf, UserCascade, DiscoveryDocument
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/models/` and `updates/`: the six tables and the initial-key seeder
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/config/`: `login_url`, `key_dir`, TTLs, grace hours
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/views/`: consent.htm, error.htm
+- `/media/nvme/dev/jz/wavepath.org/plugins/golem15/oauthserver/tests/`: the test map source (feature, security, unit; JumbojettInteropTest)
+
+### Why this phase exists
+- `/media/nvme/dev/jz/wavepath.org/.planning/notes/summercms-framework-asks-for-wavepath.md` §A1: WavePath's ask
+- `.planning/notes/wavepath-framework-asks-division.md`: where the asks land, 19.x before the cutover
+
+### Framework code being extended
+- `modules/wristband/README.md`: current API, options, security properties
+- `.planning/notes/oauth2-wristband-direct-port.md`: why wristband was written by hand (input to D-16)
+- `.planning/todos/pending/wristband-neutral-resource-default.md`: folded todo
+- `../fonoteka.go/plugins/golem15/fonoteka/classes/auth/oauth_store.go`: the existing `wristband.Backend` adapter that must keep compiling unchanged (D-09)
+- `.planning/research/STACK.md`: decided dependency set; a JOSE library or zitadel/oidc needs a decision note
+
+### Related phases
+- `.planning/phases/19.3-framework-transport-and-infrastructure-for-wavepath/`: bouncer cookie transport, Redis limiter store, CORS profiles
+- `.planning/notes/2026-10-17-generic-mcp-module.md`: 19.7 signs in through wristband; it may later turn on `openid` for MCP (D-07)
+
+
+
+
+## Existing Code Insights
+
+### Reusable Assets
+- `wristband` authorize/token engine: PKCE S256, exact redirect matching with a local 400 for unknown clients, pending requests, single-use codes, refresh rotation with lineage revocation, constant-time SHA-256 hex secrets (`crypto.go`). OIDC reuses all of it.
+- `lagoon.Encrypted`: app-key-encrypted column for the private key PEM (D-10). Check its write-only semantics against the need to read the key in Go.
+- `tide`: record PHP, replay against Go (D-14); the upstream sidecars come from Phase 14.
+- `surf` named limiters on the `cooler` Redis store from 19.3 (D-18).
+- `bouncer` JWT cookie (19.3) for the signed-in user on authorize (D-04).
+- cabana YAML admin (list, form, `fileupload` for logos, colorpicker if available, settings pages, danger record actions from Phase 18) for Clients and branding.
+- Winter import pattern: `pgstripe:import-winter`, `paymentgateway:import-winter`, the sm-faq-plugin import.
+
+### Established Patterns
+- Shared plugins live in their own `sm-*-plugin` repo and are mounted as submodules. Commit and push the plugin first, then bump the pointer in each app.
+- Security code gets its own review, and threat IDs go in the plans (T-19.4-xx).
+- A new dependency needs a decision note before code lands.
+- Framework READMEs and docs name no application, and the docs checker enforces identifiers.
+
+### Integration Points
+- `sm-user-plugin`: user-delete event (cascade subjects, consents, tokens), JWT guard for `/oauth/authorizations`, user fields for claims. It already owns `/oauth/{provider}` (google|facebook|github) for social login, and the PHP route-shadow rule applies: literal `/oauth/*` OIDC paths must not collide, and 19.5 adds social login on the same prefix.
+- Framework scheduler: `oauthserver:prune` and the opt-in rotation.
+- fonoteka.go: must build and pass unchanged, apart from setting `Resource` explicitly (folded todo).
+
+
+
+
+## Specific Ideas
+
+- PHP's route header comment explains three auth models side by side (public discovery/JWKS; server-to-server token/userinfo with no session; browser authorize with session and CSRF; SPA authorizations with JWT). Keep that separation in the Go route groups.
+- `sub` is never derived from the user id, the email or any account attribute.
+- Error handling at authorize follows RFC 6749 §4.1.2.1 as in PHP and wristband: unknown client or mismatched redirect is rendered locally, never redirected.
+
+
+
+
+## Deferred Ideas
+
+- Move fonoteka's own `wristband.Backend` adapter onto `sm-oauthserver-plugin`'s stores (todo after 19.4).
+- `openid` / `id_token` for MCP clients (19.7 decides).
+
+### Reviewed Todos (not folded)
+- The keyword matcher also surfaced docs, lagoon, scaffold, FAQ/menu/sitemap and user-plugin todos. None are about OIDC or wristband, so they stay in the backlog.
+
+
+
+---
+
+*Phase: 19.4-oidc-provider-on-wristband*
+*Context gathered: 2026-10-17*
diff --git a/.planning/phases/19.4-oidc-provider-on-wristband/19.4-DISCUSSION-LOG.md b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-DISCUSSION-LOG.md
new file mode 100644
index 00000000..65c58bde
--- /dev/null
+++ b/.planning/phases/19.4-oidc-provider-on-wristband/19.4-DISCUSSION-LOG.md
@@ -0,0 +1,134 @@
+# Phase 19.4: OIDC provider on wristband - 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-10-17
+**Phase:** 19.4-oidc-provider-on-wristband
+**Areas discussed:** Where the pieces live, MCP and OIDC together, Signing keys, Parity bar
+
+---
+
+## Where the pieces live
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| wristband + sm-oauthserver-plugin | Protocol in wristband; stores, tables, import, admin, branding, pages, commands in a new shared plugin | ✓ |
+| Everything in the plugin | Port PHP 1:1 including the protocol; wristband stays MCP-only | |
+| Protocol only in 19.4 | wristband OIDC with fakes; the rest waits for the WavePath milestone | |
+
+**User's choice:** wristband + sm-oauthserver-plugin.
+**Notes:** The user first asked why the protocol should stay in wristband when it serves nothing without the plugin. Answer given: wristband is already the shared, reviewed engine (fonoteka's MCP flow runs on it through its own GORM adapter); OIDC reuses most of authorize/token; and 19.7, a framework module, signs in through wristband and cannot import a plugin.
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Yes, one store for both | Plugin tables cover MCP (DCR) and OIDC (admin) clients | ✓ |
+| OIDC only | MCP-only apps keep writing adapters | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Plugin renders HTML | Server-rendered branded consent/error pages like PHP | |
+| SPA renders, plugin serves JSON | Like wristband's `/connect` flow | |
+| Both, configurable | Default HTML, option to redirect to the app's page | ✓ |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Keep PHP handoff | Login URL + `?redirect=/oauth/authorize/resume/{id}`, user from `token` cookie | ✓ |
+| Plugin's own login form | Server-rendered login inside the plugin | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Require sm-user-plugin | Direct access to users, delete event, JWT guard | ✓ |
+| Claims interface | Plugin-defined provider interface | |
+
+---
+
+## MCP and OIDC together
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Two Server values, one engine | Two configured Servers sharing Backend and issuer | ✓ |
+| One Server, two profiles | Single Server with an OIDC section | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| No, a client has a kind | kind=mcp\|oidc, endpoints reject the other kind | ✓ |
+| Shared clients | Any client may use either flow | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Not in 19.4 | MCP unchanged; 19.7 may enable openid | ✓ |
+| Yes, opt-in per Server | Options switch for id_tokens on MCP | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Additive, separate OIDC interfaces | Required only when OIDC is enabled; fonoteka unchanged | ✓ |
+| Extend Tx, update fonoteka | Breaking change for every adapter | |
+
+---
+
+## Signing keys
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Database, private key encrypted | Keys table, app-key encryption, import keeps kids | ✓ |
+| Files like PHP | Same layout, needs a shared volume for several instances | |
+| DB default, files optional | Both KeyStore implementations | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| RS256 default, ES256 configurable | Imported keys stay RS256 | ✓ |
+| RS256 only | Exactly PHP | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Command plus optional schedule | rotate-keys, 24h grace, opt-in scheduled rotation | ✓ |
+| Manual command only | Exactly PHP | |
+
+**Notes:** PHP hashes client secrets, codes and access tokens with SHA-256 hex, the same as wristband, so import is a straight copy (researcher to confirm).
+
+---
+
+## Parity bar
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| RP-visible contract via tide | Discovery, JWKS, token/userinfo JSON, errors, id_token claims, redirects; PHP test map | ✓ |
+| Byte parity everywhere | Including consent/error HTML | |
+| Spec conformance only | Conformance + interop, PHP parity where RPs depend on it | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Public default, pairwise per client | Imported subs public; per-client pairwise opt-in | ✓ |
+| Public only, like PHP | Drop pairwise, amend roadmap | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| None, like PHP | Code grant only | |
+| Opt-in per client via offline_access | Existing rotating refresh tokens | ✓ |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Hand-written, stdlib + one JOSE lib | Extend wristband; RP library in tests only | |
+| Let research decide | Researcher compares with zitadel/oidc v3; the note records it | ✓ |
+| zitadel/oidc v3 behind wristband | Certified library, adapt stores | |
+
+| Option | Description | Selected |
+|--------|-------------|----------|
+| Same buckets and keys | PHP limits as named surf limiters on the Redis store | ✓ |
+| You decide | Planner picks limits | |
+
+---
+
+## Claude's Discretion
+
+- Table names, Options/interface names, cabana screen layout, the config spelling of the consent-redirect option, the default scheduled-rotation interval.
+
+## Deferred Ideas
+
+- Move fonoteka's own wristband adapter onto the plugin's stores.
+- openid/id_token for MCP clients (19.7).
+
+## Folded Todos
+
+- Neutral default resource URL in `wristband.DefaultOptions`.