slack5-1770603014/.sdlc/features/user-preferences/spec.md
rdev-worker 07294fcf8f
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
2026-02-09 02:18:10 +00:00

5.8 KiB

Feature: User Preferences API

Problem Statement

Users need a way to persist and retrieve their personal preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently, the preferences-api service exists as a scaffold with example CRUD endpoints but no real domain logic or database persistence. This feature replaces the example scaffolding with a real user preferences domain.

User Stories

  • As a user, I want to save my theme preference so that the UI renders in my chosen theme across sessions.
  • As a user, I want to save my language preference so that the application displays content in my preferred language.
  • As a user, I want to configure my notification settings so that I only receive the notifications I care about.
  • As a frontend application, I want to retrieve all preferences for a user in a single request so that I can initialize the UI efficiently.
  • As a frontend application, I want to update one or more preferences in a single request so that partial updates are supported without overwriting unrelated settings.

API Design

Endpoints

Method Path Description
GET /api/preferences-api/preferences/{user_id} Retrieve all preferences for a user
PUT /api/preferences-api/preferences/{user_id} Create or update preferences for a user (upsert)

GET /preferences/{user_id}

Response 200:

{
  "data": {
    "user_id": "550e8400-e29b-41d4-a716-446655440000",
    "preferences": {
      "theme": "dark",
      "language": "en",
      "notifications": {
        "email": true,
        "push": false,
        "digest": "weekly"
      }
    },
    "updated_at": "2026-02-09T12:00:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}

Response 404: User has no preferences saved (returns default preferences instead — see Open Questions).

PUT /preferences/{user_id}

Upserts preferences. Merges provided keys with existing preferences (partial update). Keys not included in the request body are left unchanged.

Request Body:

{
  "preferences": {
    "theme": "light",
    "notifications": {
      "push": true
    }
  }
}

Response 200: Returns the full merged preference set after update.

Preference Keys

Key Type Allowed Values Default
theme string "light", "dark", "system" "system"
language string BCP 47 language tag (e.g., "en", "fr", "es") "en"
notifications.email boolean true, false true
notifications.push boolean true, false true
notifications.digest string "daily", "weekly", "never" "weekly"

Acceptance Criteria

  • GET /preferences/{user_id} returns stored preferences for the given user
  • GET for a user with no saved preferences returns default preference values (not 404)
  • PUT /preferences/{user_id} creates preferences if none exist (upsert)
  • PUT /preferences/{user_id} merges provided keys with existing preferences (partial update)
  • PUT validates theme against allowed values (light, dark, system)
  • PUT validates language is a non-empty string
  • PUT validates notifications.digest against allowed values (daily, weekly, never)
  • PUT validates notifications.email and notifications.push are booleans
  • Invalid preference values return 400 Bad Request with descriptive error details
  • Invalid user_id format (non-UUID) returns 400 Bad Request
  • All responses use the standard {data, meta} envelope
  • OpenAPI spec documents both endpoints with request/response schemas
  • Preferences are persisted in PostgreSQL (survive service restarts)
  • Database migration creates the preferences table
  • Domain logic is separated from HTTP handlers (hexagonal architecture)
  • Service and handler layers have unit tests
  • Existing example scaffolding is removed and replaced with preferences domain

Technical Constraints

  • Must follow the existing hexagonal architecture pattern: domain -> service -> port -> adapter
  • Must use app.Wrap() for handler error returns, app.BindAndValidate() for request binding
  • Must use httpresponse.OK / httpresponse.Created for response envelope
  • Must use httperror.* for HTTP error types
  • Must use {param} brace syntax for chi URL parameters (not :param)
  • Must use pkg/database for PostgreSQL connection
  • Database migration files go in services/preferences-api/migrations/
  • Preferences stored as JSONB in PostgreSQL for flexible schema
  • user_id is a UUID provided by the caller (no user management in this service)

Dependencies

  • pkg/database package for PostgreSQL connectivity
  • pkg/app, pkg/httpresponse, pkg/httperror for HTTP patterns (already in use)
  • PostgreSQL database instance (connection via DATABASE_URL env var, already in config)
  • No external service dependencies

Out of Scope

  • User authentication/authorization (auth middleware exists but user identity validation is not part of this feature)
  • User management or user creation
  • Preference change history/audit log
  • Preference schemas beyond theme, language, and notifications
  • Real-time preference sync (WebSocket push on change)
  • Bulk operations across multiple users
  • Rate limiting

Open Questions

  1. Default preferences on GET for unknown user: Should GET for a user with no saved preferences return a 200 with defaults, or a 404? (Spec assumes 200 with defaults for better frontend DX.)
  2. Unknown preference keys: Should PUT reject unknown keys not in the defined schema, or silently accept them for forward compatibility?
  3. User ID validation beyond format: Should the service verify the user_id corresponds to a real user (via inter-service call), or accept any valid UUID?