# Feature: User Preferences API ## Problem Statement Users need the ability to store and retrieve personal preferences (theme, language, notification settings) so that their experience is personalized and consistent across sessions. Currently, the preferences-api service has only scaffolded example endpoints with no real preference management functionality. ## User Stories - As an application user, I want to retrieve my preferences so that the UI reflects my chosen theme, language, and notification settings. - As an application user, I want to update my preferences so that changes persist across sessions. - As a frontend application, I want to fetch preferences by user ID on login so that I can render the correct theme and locale immediately. - As a system administrator, I want preferences stored as structured key-value pairs so that new preference types can be added without schema changes. ## Acceptance Criteria - [ ] `GET /api/preferences-api/preferences/{user_id}` returns all preferences for a user as key-value pairs - [ ] `GET /api/preferences-api/preferences/{user_id}` returns `200` with empty preferences object when user has no stored preferences - [ ] `GET /api/preferences-api/preferences/{user_id}` returns `404` only if user_id format is invalid (not a valid UUID) - [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or updates preferences (upsert behavior) - [ ] `PUT /api/preferences-api/preferences/{user_id}` accepts a JSON body with a `preferences` object containing key-value pairs - [ ] `PUT /api/preferences-api/preferences/{user_id}` returns `200` with the updated preferences - [ ] `PUT /api/preferences-api/preferences/{user_id}` validates known preference keys against allowed values: - `theme`: `"light"`, `"dark"`, `"system"` - `language`: valid BCP-47 language tag (e.g., `"en"`, `"fr"`, `"es"`, `"de"`, `"ja"`) - `notifications_enabled`: `true` or `false` - [ ] `PUT /api/preferences-api/preferences/{user_id}` returns `400` with details when validation fails - [ ] Unknown preference keys are accepted and stored (extensibility for future preference types) - [ ] Preferences are persisted in PostgreSQL and survive service restarts - [ ] All responses follow the `{data, meta}` envelope pattern - [ ] OpenAPI spec documents both endpoints with request/response schemas - [ ] Handler tests cover success paths and error cases - [ ] Service-layer tests cover business logic (validation, upsert behavior) - [ ] Database migration creates the preferences table ## 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_enabled": true }, "updated_at": "2026-02-09T12:00:00Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ### PUT /api/preferences-api/preferences/{user_id} **Request Body:** ```json { "preferences": { "theme": "dark", "language": "fr", "notifications_enabled": false } } ``` **Response 200:** ```json { "data": { "user_id": "550e8400-e29b-41d4-a716-446655440000", "preferences": { "theme": "dark", "language": "fr", "notifications_enabled": false }, "updated_at": "2026-02-09T12:01:00Z" }, "meta": { "request_id": "...", "timestamp": "..." } } ``` **Response 400 (validation error):** ```json { "error": { "code": "VALIDATION_ERROR", "message": "Invalid preference values", "details": { "theme": "must be one of: light, dark, system" } }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ## Technical Constraints - Must follow existing hexagonal architecture: domain -> service -> port -> adapter - Must use `app.Wrap()` handler pattern, `httpresponse.*` envelope, `httperror.*` error types - Must use `{param}` brace syntax for chi URL parameters (never colon syntax) - Database adapter uses `pkg/database` (sqlx) with embedded SQL migrations - Preferences stored as JSONB column in PostgreSQL for flexible key-value storage - `user_id` is a UUID path parameter (validated at handler level) - PUT is idempotent: creates preferences row if none exists, updates if it does (upsert via `ON CONFLICT`) - Service runs on port 8001 under route prefix `/api/preferences-api` ## Database Schema ```sql -- migrations/001_create_preferences.sql CREATE TABLE IF NOT EXISTS preferences ( user_id UUID PRIMARY KEY, preferences JSONB NOT NULL DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_preferences_updated_at ON preferences (updated_at); ``` ## Hexagonal Architecture Mapping | Layer | File | Responsibility | |-------|------|----------------| | Domain | `internal/domain/preference.go` | `UserPreferences` model, validation rules for known keys | | Port | `internal/port/preference.go` | `PreferenceRepository` interface (`Get`, `Upsert`) | | Service | `internal/service/preference.go` | `PreferenceService` with validation + delegation to port | | Adapter | `internal/adapter/postgres/preference.go` | PostgreSQL implementation of `PreferenceRepository` | | Handler | `internal/api/handlers/preference.go` | HTTP handlers for GET/PUT endpoints | | Routes | `internal/api/routes.go` | Register preference routes | | Spec | `internal/api/spec.go` | OpenAPI documentation for preference endpoints | ## Dependencies - PostgreSQL database (already configured via `DATABASE_URL` in `.env.example`) - `pkg/database` package for connection pooling and migrations - `pkg/app`, `pkg/httperror`, `pkg/httpresponse` for handler patterns - `pkg/openapi` for API documentation - Existing preferences-api service scaffolding ## Out of Scope - Authentication/authorization enforcement (auth is opt-in per CLAUDE.md; can be layered on later) - Per-preference-key endpoints (e.g., `GET /preferences/{user_id}/theme`) - full object only - Preference defaults management (hardcoded defaults in frontend, not API) - Preference change history/audit log - Bulk preference operations across multiple users - Real-time preference change notifications (WebSocket/SSE) - DELETE endpoint (preferences are upserted, not deleted) ## Open Questions 1. **Should unknown preference keys have value-type validation?** Currently spec allows any JSON value for unknown keys. Should we restrict to strings/booleans/numbers only? 2. **Should there be a maximum number of preference keys per user?** A limit would prevent abuse but adds complexity. 3. **Should the API enforce that `user_id` corresponds to a real user?** This requires a call to an auth/user service. Current spec treats `user_id` as an opaque UUID with no cross-service validation. 4. **Should preference values have a max size limit?** JSONB columns can hold large values; a per-value or total-size limit may be prudent.