# Feature: User Preferences API ## Problem Statement Users need a way to persist and retrieve their application preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently there is no preferences storage — the `preferences-api` service exists as a skeleton with only the example CRUD scaffold. This feature replaces the example entity with a real user preferences system. ## User Stories - As an authenticated user, I want to save my theme preference so that the UI renders in my chosen theme across sessions. - As an authenticated user, I want to save my language preference so that content is displayed in my preferred language. - As an authenticated user, I want to configure my notification settings so that I only receive the alerts I care about. - As an authenticated user, I want to retrieve all my preferences in a single call so that the frontend can initialize quickly. - As an API consumer, I want to update individual preference keys without overwriting unrelated settings. ## API Design ### Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/api/preferences-api/preferences/{user_id}` | Required | Retrieve all preferences for a user | | PUT | `/api/preferences-api/preferences/{user_id}` | Required | Create or update preferences for a user (merge semantics) | ### Request / Response **GET /api/preferences-api/preferences/{user_id}** Response `200 OK`: ```json { "data": { "user_id": "uuid", "preferences": { "theme": "dark", "language": "en", "notifications": { "email": true, "push": false, "digest": "weekly" } }, "updated_at": "2026-02-08T00:00:00Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` Response `404 Not Found` (no preferences saved yet): ```json { "error": { "code": "NOT_FOUND", "message": "preferences not found" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` **PUT /api/preferences-api/preferences/{user_id}** Request body: ```json { "preferences": { "theme": "light", "notifications": { "email": false } } } ``` Response `200 OK` (returns merged result): ```json { "data": { "user_id": "uuid", "preferences": { "theme": "light", "language": "en", "notifications": { "email": false, "push": false, "digest": "weekly" } }, "updated_at": "2026-02-08T00:00:01Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ### Merge Semantics PUT performs a **shallow merge** at the top-level keys (`theme`, `language`, `notifications`). Nested objects like `notifications` are replaced entirely when provided. This keeps behavior predictable without requiring JSON Patch complexity. ## Acceptance Criteria - [ ] GET `/api/preferences-api/preferences/{user_id}` returns `200` with stored preferences - [ ] GET returns `404` when no preferences exist for the user - [ ] PUT `/api/preferences-api/preferences/{user_id}` creates preferences if none exist (upsert) - [ ] PUT merges provided keys with existing preferences (shallow merge) - [ ] PUT returns `200` with the full merged preference set - [ ] PUT validates that `preferences` field is present and is a JSON object - [ ] PUT rejects unknown top-level preference keys with `400 Bad Request` - [ ] Both endpoints require authentication via `auth.Middleware()` - [ ] Authenticated user can only access their own preferences (user_id in path must match token subject), unless they have an admin role - [ ] `user_id` path parameter is validated as a UUID - [ ] Preferences are persisted in-memory via the existing adapter pattern (database adapter deferred) - [ ] OpenAPI spec documents both endpoints with schemas, examples, and error responses - [ ] Domain model defines allowed preference keys and validation rules - [ ] Handler tests cover success paths, validation errors, auth failures, and not-found cases - [ ] Service tests cover merge logic, create-on-first-PUT, and authorization checks - [ ] All existing `example` scaffold code is removed and replaced with preferences code ## Data Model ### Domain Entity: `Preference` ```go type UserID string type Preferences struct { UserID UserID Theme string // "light", "dark", "system" Language string // ISO 639-1 code: "en", "es", "fr", etc. Notifications NotificationSettings UpdatedAt time.Time } type NotificationSettings struct { Email bool Push bool Digest string // "daily", "weekly", "never" } ``` ### Allowed Values | Key | Type | Allowed Values | Default | |-----|------|----------------|---------| | `theme` | string | `light`, `dark`, `system` | `system` | | `language` | string | ISO 639-1 codes | `en` | | `notifications.email` | bool | `true`, `false` | `true` | | `notifications.push` | bool | `true`, `false` | `true` | | `notifications.digest` | string | `daily`, `weekly`, `never` | `weekly` | ## Technical Constraints - Must follow the existing hexagonal architecture: domain → service → port → adapter - Must use `app.Wrap()`, `app.BindAndValidate()`, `httpresponse.*`, `httperror.*` patterns - Must use `auth.Middleware()` for protected routes - In-memory adapter for initial implementation (matches existing pattern); database migration deferred to a follow-up feature - Preference values must be validated against allowed values in the domain layer - OpenAPI spec must be updated to replace example endpoints with preference endpoints - Route base path remains `/api/preferences-api` ## Dependencies - `pkg/app` — handler wrapping, request binding - `pkg/auth` — JWT middleware, context user extraction - `pkg/httperror` — typed HTTP errors - `pkg/httpresponse` — response envelope - `pkg/httpvalidation` — struct validation - `pkg/openapi` — spec builder - `pkg/logging` — structured logging - Existing `preferences-api` service skeleton (to be modified in-place) ## Out of Scope - Database persistence (PostgreSQL adapter) — separate follow-up feature - Bulk preference operations across multiple users - Preference history / audit log - Preference defaults management API (defaults are hardcoded in domain) - Frontend integration (consuming the API from apps/) - Rate limiting - Preference change webhooks / event publishing ## Open Questions 1. **Authorization model**: Should any user be able to read another user's preferences, or is it strictly own-user-only? The spec assumes own-user + admin override, but this needs confirmation. 2. **Additional preference keys**: Are `theme`, `language`, and `notifications` the complete set, or should the schema be extensible for future keys without code changes? 3. **Default preferences**: When GET returns 404, should we instead return a `200` with default values? This simplifies the frontend but changes the semantic contract.