# Phase 9: Backend admin authentication and schema pipeline - 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-23 **Phase:** 09-backend-admin-authentication-and-schema-pipeline **Areas discussed:** Admin identity and login, Schema pipeline shape, Admin CRUD API contract, Relation manager and settings --- ## Admin identity and login | Option | Description | Selected | |--------|-------------|----------| | Winter-shaped tables | backend_users + backend_user_roles, groups skipped, is_superuser and role codes kept | ✓ | | Lean admin model | One summer_admins table with role string and permissions JSON | | | Reuse frontend users table with a flag | is_backend + role on users | | **User's choice:** Winter-shaped tables | Option | Description | Selected | |--------|-------------|----------| | JWT via a `backend` bouncer guard | Second guard beside `jwt`, own secret and audience, reuses Phase 7 mint/verify/blacklist | ✓ | | Cookie sessions with scs | HttpOnly cookie, Postgres session store, CSRF | | | You decide | | | **User's choice:** JWT via a `backend` bouncer guard | Option | Description | Selected | |--------|-------------|----------| | Winter strings with wildcard | HasPermissions in registerPermissions shape, is_superuser, role JSON, `.*` prefix match, RequiredPermissions() enforced by framework | ✓ | | Flat permission list, no wildcards | Exact match only | | | You decide | | | **User's choice:** Winter strings with wildcard | Option | Description | Selected | |--------|-------------|----------| | Console command | `summer admin:create` + `admin:reset-password` | ✓ | | Seeded from config | Boot-time superuser from YAML/env | | | Both | Command canonical, env seed opt-in | | **User's choice:** Console command --- ## Schema pipeline shape | Option | Description | Selected | |--------|-------------|----------| | Winter layout | config_form/config_list point at models//fields.yaml and columns.yaml; scaffold updated | ✓ | | Scaffold layout | Keep controllers//fields.yaml as Phase 4 emits | | | You decide | | | **User's choice:** Winter layout | Option | Description | Selected | |--------|-------------|----------| | Typed schema, Winter key names | Go structs per field type, unknown keys fail, Winter spelling in JSON | ✓ | | Pass-through map | Generic map served as-is | | | Typed schema, normalized names | snake_case house style | | **User's choice:** Typed schema, Winter key names | Option | Description | Selected | |--------|-------------|----------| | Resolved server-side | phrasebook resolves labels in the admin's locale; SPA gets strings | ✓ | | Raw keys plus a lang bundle endpoint | SPA resolves locally | | | Both fields | label and labelKey side by side | | **User's choice:** Resolved server-side | Option | Description | Selected | |--------|-------------|----------| | Options provider interface on the model | DropdownOptions(field) []Option; static YAML maps also work | ✓ | | Registry keyed by method name | Controller registers a map of func by name | | | You decide | | | **User's choice:** Options provider interface on the model --- ## Admin CRUD API contract | Option | Description | Selected | |--------|-------------|----------| | /_admin/api/v1, plain JSON, no envelope | Plain resource bodies, `{message, errors}` errors | | | Reuse the fonoteka house envelope | Same helpers as /_fonoteka/api/v1 | | | You decide | | | **User's choice:** Free text: "Do you really recommend 1? Our chance to introduce nice and consistent api layer." Claude proposed a framework-wide layer (plugin-namespaced paths, one envelope with a fixed error-code vocabulary, one list contract, one verb set, relation endpoints under the record, swag-annotated framework package). User: "Yes, let's proceed with that." **Notes:** Recorded as D-09 to D-11. | Option | Description | Selected | |--------|-------------|----------| | Winter list features only | search, sort, dir, page, recordsPerPage, relation columns | | | Add per-column filters | filter[column]=value exact matches | | | You decide | | | **User's choice:** Free text: "Filters in Winter are done with config_filters.yaml we need filters like that here." Claude found the user and journal plugins' config_filter.yaml files (switch, daterange, model-backed scope) and asked about scope coverage and raw SQL `conditions:`. User: "Port 3 shapes now, note for future phases that this will be extended and all shapes will be introduced in time. Model scope methods instead of SQL strings are fully acceptable." **Notes:** Recorded as D-12; remaining scope types deferred. | Option | Description | Selected | |--------|-------------|----------| | Optional interfaces on the controller | Type-asserted hooks with admin principal in context; bulk delete per record | ✓ | | Struct of func fields | Optional func fields on AdminController | | | You decide | | | **User's choice:** Optional interfaces on the controller | Option | Description | Selected | |--------|-------------|----------| | Port the email mapping | Admin email must match exactly one frontend user; reuse its active collection; 422 otherwise | ✓ | | Explicit collection picker | SPA header on every request | | | Both | Mapping default, header override for superusers | | **User's choice:** Port the email mapping --- ## Relation manager and settings | Option | Description | Selected | |--------|-------------|----------| | config_relation.yaml as a first-class schema | Typed relation schema; `type: relation-manager` replaces `partial` | ✓ | | Keep `partial` as an alias | Map partial to relation manager | | | You decide | | | **User's choice:** config_relation.yaml as a first-class schema | Option | Description | Selected | |--------|-------------|----------| | Pivot fields stamped by the framework hook | RelationBeforeLink stamps role, granted_at, granted_by; unlink deletes pivot | ✓ | | Bare pivot insert | Only the two foreign keys | | | You decide | | | **User's choice:** Pivot fields stamped by the framework hook | Option | Description | Selected | |--------|-------------|----------| | HasSettings capability + singleton model | registerSettings shape, /settings/{code} GET/PUT via Fillable | ✓ | | Settings as an ordinary admin controller | Form with no list, hand-written per page | | | You decide | | | **User's choice:** HasSettings capability + singleton model | Option | Description | Selected | |--------|-------------|----------| | HasNavigation in Winter's shape, URL becomes a route key | url replaced by controller ID; /navigation filtered by permissions | ✓ | | Winter shape with literal URLs | Keep url strings | | | You decide | | | **User's choice:** HasNavigation in Winter's shape, URL becomes a route key --- ## Claude's Discretion - Package naming and layout for the admin API and schema packages. - YAML parse timing (boot vs lazy), `rules:` in fields.yaml vs model Rules(). - Admin login throttling and password policy. - Soft-deleted rows in admin lists; settings row creation timing; relation candidate pagination. ## Deferred Ideas - Remaining Winter filter scope types. - backend_user_groups, preferences, access logs. - Policy-based row/field-level permissions. - Boot-time admin seed from environment.