# Feature: User Preferences API ## Problem Statement Users of the platform have no way to persist their personal settings (theme, language, notification preferences). Without a preferences API, every client must manage settings locally, leading to inconsistent experiences across devices and sessions. The `preferences-api` service currently contains only scaffold/example code and needs to be replaced with real preference management functionality. ## User Stories - As a user, I want to save my preferred theme so that the UI looks the same every time I log in. - As a user, I want to set my preferred language so that content is displayed in my language across devices. - As a user, I want to configure my notification settings so that I only receive the alerts I care about. - As a frontend developer, I want a simple GET/PUT API to read and write user preferences so that I can build settings pages without managing local storage. ## Acceptance Criteria - [ ] `GET /api/preferences-api/preferences/{user_id}` returns all preferences for a user as key-value pairs - [ ] `GET` returns a default set of preferences (`theme`, `language`, `notifications`) when the user has no saved preferences - [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or updates preferences for a user (upsert semantics) - [ ] `PUT` validates that `theme` is one of: `light`, `dark`, `system` - [ ] `PUT` validates that `language` is a valid BCP-47 language tag (e.g., `en`, `fr`, `es`, `de`, `ja`) - [ ] `PUT` validates that `notifications` is an object with boolean fields: `email`, `push`, `in_app` - [ ] `PUT` supports partial updates — omitted keys retain their current values - [ ] All responses follow the `{data, meta}` envelope pattern - [ ] Preferences are persisted in PostgreSQL (not in-memory) - [ ] Database migration creates the preferences table - [ ] `user_id` is validated as a UUID format - [ ] `PUT` on protected route group (requires auth when `AUTH_ENABLED=true`) - [ ] `GET` on protected route group (requires auth when `AUTH_ENABLED=true`) - [ ] Handler tests cover success, validation errors, not-found defaults, and invalid user ID - [ ] Service-layer tests cover business logic with mock repository - [ ] Domain model enforces preference value constraints - [ ] OpenAPI spec documents both endpoints with request/response schemas - [ ] Existing Example CRUD scaffold code is removed ## API Design ### GET /api/preferences-api/preferences/{user_id} **Response 200:** ```json { "data": { "user_id": "550e8400-e29b-41d4-a716-446655440000", "preferences": { "theme": "dark", "language": "en", "notifications": { "email": true, "push": false, "in_app": true } }, "updated_at": "2026-01-15T10:30:00Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ### PUT /api/preferences-api/preferences/{user_id} **Request body (partial update):** ```json { "theme": "dark", "notifications": { "email": false } } ``` **Response 200:** ```json { "data": { "user_id": "550e8400-e29b-41d4-a716-446655440000", "preferences": { "theme": "dark", "language": "en", "notifications": { "email": false, "push": true, "in_app": true } }, "updated_at": "2026-02-08T12:00:00Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ## Default Preference Values | Key | Default Value | |-----------------|---------------| | `theme` | `system` | | `language` | `en` | | `notifications.email` | `true` | | `notifications.push` | `true` | | `notifications.in_app` | `true` | ## Technical Constraints - **Database:** PostgreSQL via `pkg/database` (connection pool, migrations, transactions) - **Architecture:** Hexagonal — domain, service, port (interface), adapter (postgres implementation) - **Router:** chi with `{param}` brace syntax for URL parameters - **Validation:** `pkg/httpvalidation` with `go-playground/validator` struct tags - **Error handling:** Domain errors mapped to HTTP errors via `httperror.*` factories - **Response format:** All responses wrapped via `httpresponse.OK`, `httpresponse.Created`, etc. - **Auth:** Opt-in via `auth.Middleware()` in route group, controlled by `AUTH_ENABLED` env var - **Migrations:** SQL files in `services/preferences-api/migrations/` using `pkg/database.RunMigrations` - **Port:** Service runs on port 8001 (existing allocation) ## Database Schema ```sql CREATE TABLE IF NOT EXISTS user_preferences ( user_id UUID PRIMARY KEY, theme VARCHAR(10) NOT NULL DEFAULT 'system', language VARCHAR(10) NOT NULL DEFAULT 'en', notif_email BOOLEAN NOT NULL DEFAULT true, notif_push BOOLEAN NOT NULL DEFAULT true, notif_in_app BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` ## Dependencies - PostgreSQL database available (connection string via `DATABASE_URL` env var) - `pkg/database` package for connection pooling and migrations - `pkg/app`, `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` packages (all exist) - `pkg/auth` for JWT middleware (exists) ## Out of Scope - User creation/management (user IDs come from an external auth system) - Preference history/audit log - Preference schemas beyond `theme`, `language`, `notifications` - Admin endpoints to manage preferences for other users - Real-time preference sync via WebSocket - Preference import/export ## Open Questions 1. **Authorization model:** Should `GET /preferences/{user_id}` be restricted so users can only read their own preferences, or can any authenticated user read any user's preferences? (Spec assumes auth-protected but no ownership check — simplest to start.) 2. **Extensible keys:** Should the API support arbitrary preference keys beyond the three defined, or should it be a fixed schema? (Spec assumes fixed schema for type safety and validation.) 3. **Rate limiting:** Should PUT be rate-limited to prevent abuse? (Spec assumes no — can be added later via middleware.)