Phase 7: User plugin and authentication - 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-22
Phase: 7-User plugin and authentication
Areas discussed: Route surface cut, JWT lifecycle, Parity evidence, Security vs parity
Route surface cut
Core routes
| Option |
Description |
Selected |
| Nuxt-used + criterion 1 |
login, logout, fetch, refresh, register, update, change-password, marketing-consent + forgot/reset/activate/activate-by-code |
✓ |
| Nuxt-used only |
Drop forgot/reset/activate; conflicts with criterion 1 |
|
| Everything live in routes.php |
Also api/user/batch and /_user/activate/{id} |
|
User's choice: Asked first which endpoints option 1 skips versus option 3. Answer given: exactly two — GET /api/user/batch (presence, no caller) and GET /_user/activate/{id} (signed-URL legacy theme flow). Then: "OK option 1 then, we'll build presence in future milestones, legacy cms theme flow is legacy."
Social login
| Option |
Description |
Selected |
| oauth-providers only, rest deferred |
Login page renders; provider flow deferred |
✓ |
| Port the full social flow now |
Redirect, callback, complete endpoints, bootstrap OTP |
|
| Defer all of it |
oauth-providers 404s |
|
Avatar
| Option |
Description |
Selected |
| Port both now |
Multipart upload on Phase 5 attachments + Thumb(128) |
✓ |
| Payload fields only |
Endpoints wait for Phase 12 |
|
Dead routes (PIN, device, 2FA)
| Option |
Description |
Selected |
| Skip entirely, document |
No 501 shells; login never returns two_factor_required |
✓ |
| Skip, but port the 2FA login branch |
Enrolled accounts not downgraded at cutover |
|
JWT lifecycle
Blacklist storage
| Option |
Description |
Selected |
| Postgres table |
Survives restarts, works across replicas |
✓ |
| In-process map |
Restart resurrects logged-out tokens |
|
| Postgres + in-process read cache |
More moving parts |
|
Cutover compatibility
| Option |
Description |
Selected |
| Yes, wire-compatible tokens |
Same secret and claims; PHP blacklist not migrated |
✓ |
| Compatible claims, but no promise |
Cutover may force re-login |
|
| Yes, and migrate the PHP blacklist |
Phase 15 import |
|
| Option |
Description |
Selected |
| Port the split exactly |
Incl. ?jwt_token= and jwt-auth default parsers |
|
| One permissive extractor everywhere |
Cookies on /_user too |
|
| Header + cookies only, everywhere (follow-up) |
Drop all URL/body/route-param sources |
✓ |
| Drop URL sources, keep POST-body jwt_token (follow-up) |
|
|
| Drop, but answer explicitly with 400 (follow-up) |
|
|
User's choice: Free text — "jwt_token GET param should be depreciated by now per Golem15 Stack security upgrade, we don't want tokens in access logs. But we need to port the split for Bearer / Cookies as it's used trough all projects."
Notes: Verified neither the Nuxt app nor fonoteka-mcp sends jwt_token/?token=; PHP's $request->get('jwt_token') also reads the POST body. Follow-up confirmed "Header + cookies only, everywhere".
TTLs
| Option |
Description |
Selected |
| Config keys, PHP-env defaults in the app |
Library defaults in plugin, Płytarium values in fonoteka.go |
✓ |
| Hardcode Płytarium's values in the plugin |
|
|
Parity evidence
| Question |
Options |
Selected |
| How is the /_user contract proven? |
Record new tide fixtures + extend manifest / Hand-written contract tests / Both |
Record new tide fixtures, extend manifest |
| Mail-dependent codes |
Seed hook reads code from DB / Capture from mail sink / Pre-seeded fixed codes |
Seed hook reads the code from the DB |
| Error-path coverage |
Every distinct status + body per route / Happy path + one failure |
Every distinct status + body per route |
| Nuxt client flow |
Add a nuxt-auth flow fixture / Route fixtures are enough |
Add a nuxt-auth flow fixture |
Security vs parity
Reset/activation codes
| Option |
Description |
Selected |
| Add expiry, keep format and columns |
TTL via added timestamp column, constant-time compare, same wire bodies |
✓ |
| Exact PHP behaviour |
No expiry |
|
| Expiry + hash at rest |
Breaks PHP-issued codes and the DB-reading seed hook |
|
User's choice: "Is your research correct? You sure PHP codes do not expire? Option 1 tho, quite sure php codes do expire"
Notes: Re-verified in source: Winter Storm User.php:258-292 is a plain === with no timestamp, the plugin does not override it, the users table has no expiry column, and passwords.expire=60 belongs to the unused Laravel broker. Things that do expire: bootstrap OTP (30 min), signed invite URL (72 h). Possible that another Golem15 project has a newer plugin version. Assumption stated: null issued-at codes get one full TTL from cutover; not objected to.
Failed-login throttle
| Option |
Description |
Selected |
| Port Winter Throttle onto user_throttle |
Durable, ban/suspend states, PHP bodies |
✓ |
| Limiter bucket only |
In-process, no ban state |
|
Login/register quirks
| Option |
Description |
Selected |
| Port restore-on-login, drop guest conversion |
|
✓ |
| Port both exactly |
|
|
| Drop both |
|
|
Mail + hashing
| Option |
Description |
Selected |
| Send inline now; bcrypt from config, rehash on login |
Seam for River in Phase 11 |
✓ |
| Inline mail; bcrypt 10 fixed, no rehash |
|
|
| Goroutine fire-and-forget mail |
|
|
Revoke tokens on password change/reset
| Option |
Description |
Selected |
| Invalidate all older tokens |
Per-user "valid after" timestamp checked by the jwt guard |
✓ |
| Exact PHP behaviour |
Old tokens live up to 30 days |
|
| Invalidate on reset only |
|
|
Setting must_change_password
| Option |
Description |
Selected |
| CLI command now, admin field in Phase 9 |
|
✓ |
| Tests and seed hooks only |
|
|
Claude's Discretion
- Locale-resolution stage placement for I18N-02
- Package homes for minting/blacklist, Store interface, sweep interval
getApiArray event type and merge order
- Minimal read models for payload fields backed by unported tables (permissions, groups, role)
- 423-exempt grouping asserted over the route table
- Column/config/command names; plan count and split
Deferred Ideas
- Presence (
api/user/batch) — future milestone
- Full social login flow and password-bootstrap OTP
- PIN login, device auth, 2FA
- Signed admin-invite activation link (legacy)
- Guest conversion (keios.eu), account deletion/GDPR routes
- River-queued mail (Phase 11), PHP blacklist migration (not done), admin lock/ban UI (Phase 9)