# Feature: User Preferences API ## Problem Statement Users of the platform need a way to persist and retrieve personal preferences (theme, language, notification settings) so their experience is consistent across sessions and devices. Currently, the `preferences-api` service exists as a scaffold with only example CRUD endpoints and no actual preference management capability. ## User Stories - As an authenticated user, I want to retrieve my preferences so that the UI reflects my saved settings when I log in. - As an authenticated user, I want to update my preferences so that changes to theme, language, or notification settings are persisted. - As a frontend application, I want to fetch preferences for the current user so I can apply theme/language/notification configuration on page load. - As an admin or service, I want to retrieve preferences for a specific user by user ID so I can provide user-specific experiences in server-rendered contexts. ## Acceptance Criteria - [ ] `GET /api/preferences-api/preferences/{user_id}` returns the user's preferences as a key-value map within the standard `{data, meta}` envelope - [ ] `PUT /api/preferences-api/preferences/{user_id}` creates or fully replaces the user's preferences (upsert semantics) - [ ] Both endpoints require authentication via the existing `auth.Middleware()` - [ ] Authenticated users can only access their own preferences (the `{user_id}` in the URL must match the JWT `UserID`/`Subject`) - [ ] Preferences are stored as key-value pairs with string keys and JSON-compatible values - [ ] The following preference keys are supported with validation: - `theme`: must be one of `"light"`, `"dark"`, `"system"` - `language`: must be a valid BCP-47 language tag (e.g., `"en"`, `"fr"`, `"es"`, `"de"`, `"ja"`) - `notifications.email`: must be a boolean - `notifications.push`: must be a boolean - `notifications.digest`: must be one of `"none"`, `"daily"`, `"weekly"` - [ ] `GET` for a user with no saved preferences returns a `200` with default values (not a 404) - [ ] Default preference values: `theme: "system"`, `language: "en"`, `notifications.email: true`, `notifications.push: true`, `notifications.digest: "weekly"` - [ ] `PUT` with unknown preference keys returns `400 Bad Request` with a descriptive error - [ ] `PUT` with invalid preference values returns `400 Bad Request` with per-field validation errors - [ ] Preferences are persisted in PostgreSQL (using the existing `DatabaseConfig`) - [ ] The domain layer contains pure preference models with validation logic, following hexagonal architecture - [ ] A `PreferenceRepository` port interface is defined, with a PostgreSQL adapter implementation - [ ] A `PreferenceService` orchestrates business logic between handler and repository - [ ] OpenAPI spec is updated to document both endpoints with schemas, examples, and error responses - [ ] Unit tests cover domain validation, service logic, and handler request/response mapping - [ ] Integration-style tests verify handler behavior using the in-memory repository adapter - [ ] The existing example CRUD scaffolding is removed and replaced with preference endpoints ## Technical Constraints - **Architecture**: Must follow the established hexagonal architecture pattern (domain -> service -> port -> adapter) already present in `services/preferences-api/` - **URL parameters**: Must use brace syntax `{user_id}` (not `:user_id`) per chi router requirements - **Request binding**: Must use `app.Bind()` / `app.BindAndValidate()` -- never raw `json.NewDecoder` - **Error handling**: Handlers return `error`, wrapped with `app.Wrap()`. Use `httperror.BadRequest`, `httperror.NotFound`, `httperror.Forbidden` etc. - **Response format**: All responses use the `{data, meta}` envelope via `httpresponse.OK`, `httpresponse.Created`, etc. - **Auth identity**: User identity is extracted from JWT via `auth.GetUser(ctx)` which returns `*auth.User` with an `ID` field - **Database**: PostgreSQL, connection details via `config.DatabaseConfig` (environment variable `DATABASE_URL`) - **Port**: The service runs on port 8001 (default) - **Route prefix**: All routes must be under `/api/preferences-api/` to match ingress routing ## Dependencies - `pkg/auth` -- JWT middleware and `auth.GetUser()` for extracting authenticated user identity - `pkg/app` -- `app.Wrap()`, `app.Bind()`, `app.BindAndValidate()` for handler patterns - `pkg/httperror` -- Typed HTTP error constructors - `pkg/httpresponse` -- Response envelope helpers - `pkg/openapi` -- OpenAPI spec builder for documentation - `pkg/config` -- `DatabaseConfig` for PostgreSQL connection parameters - PostgreSQL database instance (local dev via `docker-compose` or `scripts/dev.sh`) ## Out of Scope - **Bulk operations**: No endpoint for fetching/updating preferences for multiple users at once - **Preference history/audit log**: No versioning or history of preference changes - **Real-time sync**: No WebSocket or push notification when preferences change on another device - **Custom/arbitrary preference keys**: Only the defined keys (theme, language, notifications.*) are supported -- extensibility is deferred - **DELETE endpoint**: Preferences are reset to defaults via `PUT`, not deleted - **Pagination**: Preferences are a fixed small set per user, no pagination needed - **Admin override**: No endpoint for admins to modify another user's preferences (admin read is permitted for server-rendered contexts but write is not) ## Open Questions 1. **Admin read access**: Should admin users (identified by a role in JWT) be allowed to read other users' preferences, or should the `{user_id}` always be strictly self-only? The spec currently allows admin read but this needs confirmation. 2. **Preference key extensibility**: Should the API accept and store unknown preference keys (with a max key count limit), or strictly reject them? The spec currently rejects unknown keys. 3. **Partial updates (PATCH)**: Should a `PATCH` endpoint be added for updating individual preference keys without sending the full set, or is `PUT` (full replace) sufficient for MVP? 4. **Rate limiting**: Should preference updates be rate-limited to prevent abuse, or is auth sufficient protection?