# Feature: User Preferences API ## Problem Statement Users of the platform need a way to persist and retrieve their personal preferences (theme, language, notification settings) across sessions and devices. Currently the preferences-api service exists as a scaffold with only example CRUD endpoints and an in-memory store. There is no real preferences domain, no database persistence, and no API for managing user preferences. Application frontends need a reliable backend API to read and write per-user preference key-value pairs so that UI settings survive page refreshes, device switches, and service restarts. ## User Stories - As a **frontend application**, I want to GET a user's preferences so that I can render the UI with their chosen theme, language, and notification settings. - As a **frontend application**, I want to PUT (upsert) a user's preferences so that changes to settings are persisted immediately. - As a **platform operator**, I want preferences stored in PostgreSQL so that they survive service restarts and are backed up with the rest of the data. - As a **developer**, I want default preference values defined server-side so that new users get sensible defaults without client-side logic. ## Acceptance Criteria - [ ] `GET /api/preferences-api/preferences/{user_id}` returns all preferences for the given user as a JSON object - [ ] `GET` for a user with no stored preferences returns server-defined defaults (not 404) - [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or updates preferences for the given user (full replace of provided keys) - [ ] `PUT` is idempotent -- calling it twice with the same body produces the same result - [ ] Preferences are stored as key-value pairs in PostgreSQL - [ ] The following preference keys are supported with validation: - `theme` -- string, one of: `light`, `dark`, `system` (default: `system`) - `language` -- string, BCP 47 language tag, validated format (default: `en`) - `notifications_enabled` -- boolean (default: `true`) - [ ] Unknown preference keys in a PUT request are rejected with 400 Bad Request - [ ] `user_id` path parameter is validated as a non-empty string (UUID format) - [ ] All responses use the standard `{data, meta}` envelope - [ ] OpenAPI spec is updated with the new endpoints and schemas - [ ] Database migration creates the `user_preferences` table - [ ] Existing example CRUD endpoints and domain are removed (replaced by preferences) - [ ] Handler, service, domain, port, and adapter layers follow hexagonal architecture - [ ] Unit tests cover service logic (defaults, validation, upsert behavior) - [ ] Handler tests cover HTTP layer (request binding, error responses, status codes) ## Technical Constraints - **Database**: PostgreSQL via `pkg/database` (sqlx). Migration files in `services/preferences-api/migrations/`. - **Port**: Service runs on port 8001 (already configured). - **URL routing**: Must use brace syntax `{user_id}` (chi router). Never colon syntax. - **Error handling**: Domain errors mapped to HTTP errors via `httperror.*`. Handlers return `error`, wrapped with `app.Wrap()`. - **Request binding**: Use `app.BindAndValidate()` for PUT request body. - **Response format**: Use `httpresponse.OK()` / `httpresponse.NoContent()` for responses. - **Auth**: Auth middleware is opt-in via config. Endpoints should be in an auth-protectable route group, but auth enforcement is not required for this feature (configurable via `AUTH_ENABLED`). - **Preference storage model**: Each preference is a row in the `user_preferences` table with columns: `user_id`, `key`, `value`, `created_at`, `updated_at`. This allows adding new preference keys without schema changes. - **Defaults**: When a stored preference is missing, the API merges server-defined defaults so the response always contains all known keys. ## 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 } }, "meta": { "request_id": "...", "timestamp": "..." } } ``` ### PUT /api/preferences-api/preferences/{user_id} **Request body:** ```json { "preferences": { "theme": "dark", "language": "fr" } } ``` Only provided keys are updated. Omitted keys retain their current value (or default if never set). **Response 200:** ```json { "data": { "user_id": "550e8400-e29b-41d4-a716-446655440000", "preferences": { "theme": "dark", "language": "fr", "notifications_enabled": true } }, "meta": { "request_id": "...", "timestamp": "..." } } ``` **Error 400 (unknown key):** ```json { "error": { "code": "BAD_REQUEST", "message": "unknown preference key: font_size" }, "meta": { "..." } } ``` ## Database Schema ```sql CREATE TABLE user_preferences ( user_id UUID NOT NULL, key VARCHAR(64) NOT NULL, value TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, key) ); CREATE INDEX idx_user_preferences_user_id ON user_preferences (user_id); ``` ## Dependencies - PostgreSQL database accessible via `DATABASE_URL` environment variable - `pkg/database` for connection pooling and migrations - `pkg/app`, `pkg/httperror`, `pkg/httpresponse`, `pkg/httpvalidation` for HTTP layer - `pkg/auth` for optional authentication middleware ## Out of Scope - Per-preference-key access control (all preferences for a user are readable/writable as a unit) - DELETE endpoint for individual preferences (not in requirements) - Preference history / audit log - Bulk operations across multiple users - WebSocket push for real-time preference sync - Admin endpoints for managing preference definitions - Frontend integration (separate feature) ## Open Questions 1. **Authorization model**: Should the API enforce that `user_id` in the path matches the authenticated user's ID? Or is cross-user preference access allowed (e.g., for admin tools)? *Current assumption: no enforcement, auth is opt-in via config.* 2. **Preference value types**: Should values be typed (string/bool/number) at the API level, or stored/returned as strings with client-side parsing? *Current assumption: typed in API response (theme as string, notifications_enabled as boolean), stored as TEXT in DB with serialization.* 3. **Partial vs full update semantics**: PUT currently does partial update (merge). Should it be full replace (all keys must be provided)? *Current assumption: partial merge -- only provided keys are updated, missing keys retain current values.*