# Feature: User Preferences API ## Problem Statement Users need the ability to persist and retrieve personal preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently, the `preferences-api` service exists as scaffolding with only an example CRUD resource — there is no actual preferences domain model or persistence. ## 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 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 API call so that I can initialize the UI efficiently. - As a **backend service**, I want to read a user's preferences to personalize behavior (e.g., notification delivery channel). ## API Design ### Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/api/preferences-api/preferences/{user_id}` | Required | Get all preferences for a user | | `PUT` | `/api/preferences-api/preferences/{user_id}` | Required | Create or replace all preferences for a user | ### Request: `PUT /api/preferences-api/preferences/{user_id}` ```json { "preferences": { "theme": "dark", "language": "en", "notifications": { "email": true, "push": true, "sms": false } } } ``` ### Response: `GET /api/preferences-api/preferences/{user_id}` ```json { "data": { "user_id": "usr_abc123", "preferences": { "theme": "dark", "language": "en", "notifications": { "email": true, "push": true, "sms": false } }, "updated_at": "2026-02-08T10:30:00Z" }, "meta": { "request_id": "req_xyz", "timestamp": "2026-02-08T10:30:01Z" } } ``` ### Storage Model Preferences are stored as a JSON document per user (not individual key-value rows). This simplifies reads (single query) and writes (single upsert), and avoids N+1 patterns for users with many preferences. ### Known Preference Keys | Key | Type | Default | Validation | |-----|------|---------|------------| | `theme` | string | `"system"` | One of: `"light"`, `"dark"`, `"system"` | | `language` | string | `"en"` | BCP-47 language tag, max 10 chars | | `notifications.email` | bool | `true` | — | | `notifications.push` | bool | `true` | — | | `notifications.sms` | bool | `false` | — | The schema should be extensible — unknown keys are preserved but not validated, allowing new preference keys to be introduced without schema migrations. ## Acceptance Criteria - [ ] `GET /api/preferences-api/preferences/{user_id}` returns the user's preferences with a `200` response in the standard `{data, meta}` envelope. - [ ] `GET` returns default preferences (`theme: "system"`, `language: "en"`, `notifications: {email: true, push: true, sms: false}`) when no preferences have been saved for the user. - [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or fully replaces the user's preferences, returning `200`. - [ ] `PUT` validates known preference keys: rejects invalid `theme` values, rejects `language` values exceeding 10 characters. - [ ] `PUT` with invalid input returns `400 Bad Request` with a descriptive error in the standard error envelope. - [ ] Both endpoints require authentication via `auth.Middleware()`. - [ ] The authenticated user can only access their own preferences (the `user_id` in the path must match the authenticated user's ID), returning `403 Forbidden` otherwise. - [ ] Preferences are persisted to PostgreSQL and survive service restarts. - [ ] The database schema uses an upsert pattern — `PUT` to a new `user_id` creates the record, `PUT` to an existing `user_id` replaces it. - [ ] OpenAPI spec is updated with both endpoints, request/response schemas, and examples. - [ ] Handler, service, and domain layers follow existing hexagonal architecture patterns. - [ ] Unit tests cover: handler request/response mapping, service business logic (defaults, validation, authorization), domain validation rules. - [ ] URL parameters use brace syntax `{user_id}` (not colon syntax). ## Technical Constraints - **Architecture**: Must follow the existing hexagonal architecture (domain → service → port → adapter) established in the preferences-api service. - **Database**: PostgreSQL via `pkg/database`. Migrations embedded with `//go:embed`. Single table with JSONB column for preferences. - **Auth**: Endpoints protected by `auth.Middleware()`. User ID extracted from JWT claims via `auth.GetUser(ctx)`. - **Response format**: All responses use `httpresponse.OK/Created/NoContent` helpers for the `{data, meta}` envelope. - **Error handling**: Domain errors mapped to HTTP errors in handlers via `mapDomainError()` pattern. Use `httperror.BadRequest`, `httperror.NotFound`, `httperror.Forbidden`. - **Request binding**: Use `app.BindAndValidate()` for PUT request body. - **Router**: chi router with `{param}` brace syntax for URL parameters. - **Port**: Service runs on port 8001 (already configured). ## Dependencies - `pkg/database` — PostgreSQL connection and migration support (already exists). - `pkg/auth` — JWT middleware and user extraction (already exists). - `pkg/app`, `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` — HTTP framework (already exists). - A running PostgreSQL instance for persistence (managed via `DATABASE_URL` env var). ## Out of Scope - **Bulk preferences for multiple users** — Only single-user GET/PUT in this iteration. - **PATCH (partial update)** — Full replacement via PUT only. Partial updates may be added later. - **DELETE endpoint** — No need to delete preferences (they reset to defaults if the row is removed manually). - **Preference history/audit log** — No tracking of preference changes over time. - **Admin override** — No admin endpoint to modify another user's preferences. - **Frontend integration** — API only; frontend changes are a separate feature. - **Rate limiting** — Handled at the infrastructure layer, not in this feature. ## Open Questions 1. **Authorization model**: Should the authenticated user's ID come from the JWT `sub` claim, or is there a separate `user_id` field in the token? (Needs alignment with `auth.GetUser()` contract.) 2. **Unknown preference keys**: Should unknown keys be silently accepted and stored, or rejected with a validation error? (Spec currently assumes they are preserved.) 3. **Preference size limit**: Should there be a maximum size for the preferences JSON document to prevent abuse? (e.g., 10KB limit.)