# Feature: User Preferences API ## Problem Statement Users need a way to persist and retrieve their personal preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently, the `preferences-api` service exists as a scaffold with example CRUD endpoints but no real domain logic or database persistence. This feature replaces the example scaffolding with a real user preferences domain. ## User Stories - As a user, I want to save my theme preference so that the UI renders in my chosen theme across sessions. - As a user, I want to save my language preference so that the application displays content in my preferred language. - As a user, I want to configure my notification settings so that I only receive the notifications I care about. - As a frontend application, I want to retrieve all preferences for a user in a single request so that I can initialize the UI efficiently. - As a frontend application, I want to update one or more preferences in a single request so that partial updates are supported without overwriting unrelated settings. ## API Design ### Endpoints | Method | Path | Description | |--------|------|-------------| | GET | `/api/preferences-api/preferences/{user_id}` | Retrieve all preferences for a user | | PUT | `/api/preferences-api/preferences/{user_id}` | Create or update preferences for a user (upsert) | ### GET /preferences/{user_id} **Response 200:** ```json { "data": { "user_id": "550e8400-e29b-41d4-a716-446655440000", "preferences": { "theme": "dark", "language": "en", "notifications": { "email": true, "push": false, "digest": "weekly" } }, "updated_at": "2026-02-09T12:00:00Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` **Response 404:** User has no preferences saved (returns default preferences instead — see Open Questions). ### PUT /preferences/{user_id} Upserts preferences. Merges provided keys with existing preferences (partial update). Keys not included in the request body are left unchanged. **Request Body:** ```json { "preferences": { "theme": "light", "notifications": { "push": true } } } ``` **Response 200:** Returns the full merged preference set after update. ## Preference Keys | Key | Type | Allowed Values | Default | |-----|------|---------------|---------| | `theme` | string | `"light"`, `"dark"`, `"system"` | `"system"` | | `language` | string | BCP 47 language tag (e.g., `"en"`, `"fr"`, `"es"`) | `"en"` | | `notifications.email` | boolean | `true`, `false` | `true` | | `notifications.push` | boolean | `true`, `false` | `true` | | `notifications.digest` | string | `"daily"`, `"weekly"`, `"never"` | `"weekly"` | ## Acceptance Criteria - [ ] GET `/preferences/{user_id}` returns stored preferences for the given user - [ ] GET for a user with no saved preferences returns default preference values (not 404) - [ ] PUT `/preferences/{user_id}` creates preferences if none exist (upsert) - [ ] PUT `/preferences/{user_id}` merges provided keys with existing preferences (partial update) - [ ] PUT validates `theme` against allowed values (`light`, `dark`, `system`) - [ ] PUT validates `language` is a non-empty string - [ ] PUT validates `notifications.digest` against allowed values (`daily`, `weekly`, `never`) - [ ] PUT validates `notifications.email` and `notifications.push` are booleans - [ ] Invalid preference values return 400 Bad Request with descriptive error details - [ ] Invalid `user_id` format (non-UUID) returns 400 Bad Request - [ ] All responses use the standard `{data, meta}` envelope - [ ] OpenAPI spec documents both endpoints with request/response schemas - [ ] Preferences are persisted in PostgreSQL (survive service restarts) - [ ] Database migration creates the preferences table - [ ] Domain logic is separated from HTTP handlers (hexagonal architecture) - [ ] Service and handler layers have unit tests - [ ] Existing example scaffolding is removed and replaced with preferences domain ## Technical Constraints - Must follow the existing hexagonal architecture pattern: domain -> service -> port -> adapter - Must use `app.Wrap()` for handler error returns, `app.BindAndValidate()` for request binding - Must use `httpresponse.OK` / `httpresponse.Created` for response envelope - Must use `httperror.*` for HTTP error types - Must use `{param}` brace syntax for chi URL parameters (not `:param`) - Must use `pkg/database` for PostgreSQL connection - Database migration files go in `services/preferences-api/migrations/` - Preferences stored as JSONB in PostgreSQL for flexible schema - `user_id` is a UUID provided by the caller (no user management in this service) ## Dependencies - `pkg/database` package for PostgreSQL connectivity - `pkg/app`, `pkg/httpresponse`, `pkg/httperror` for HTTP patterns (already in use) - PostgreSQL database instance (connection via `DATABASE_URL` env var, already in config) - No external service dependencies ## Out of Scope - User authentication/authorization (auth middleware exists but user identity validation is not part of this feature) - User management or user creation - Preference change history/audit log - Preference schemas beyond theme, language, and notifications - Real-time preference sync (WebSocket push on change) - Bulk operations across multiple users - Rate limiting ## Open Questions 1. **Default preferences on GET for unknown user:** Should GET for a user with no saved preferences return a 200 with defaults, or a 404? (Spec assumes 200 with defaults for better frontend DX.) 2. **Unknown preference keys:** Should PUT reject unknown keys not in the defined schema, or silently accept them for forward compatibility? 3. **User ID validation beyond format:** Should the service verify the user_id corresponds to a real user (via inter-service call), or accept any valid UUID?