diff --git a/.sdlc/features/user-preferences/manifest.yaml b/.sdlc/features/user-preferences/manifest.yaml index ad10b55..5ff6c33 100644 --- a/.sdlc/features/user-preferences/manifest.yaml +++ b/.sdlc/features/user-preferences/manifest.yaml @@ -22,7 +22,7 @@ artifacts: status: pending path: review.md spec: - status: pending + status: draft path: spec.md tasks: status: pending diff --git a/.sdlc/features/user-preferences/spec.md b/.sdlc/features/user-preferences/spec.md new file mode 100644 index 0000000..d1ef085 --- /dev/null +++ b/.sdlc/features/user-preferences/spec.md @@ -0,0 +1,150 @@ +# 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.*