slack5-1770574304/.sdlc/features/user-preferences/spec.md
rdev-worker b2e88a92a3
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
2026-02-08 18:18:39 +00:00

78 lines
6.1 KiB
Markdown

# 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?