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