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

6.8 KiB

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:

{
  "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:

{
  "preferences": {
    "theme": "dark",
    "language": "fr",
    "notifications_enabled": false
  }
}

Response 200:

{
  "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):

{
  "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

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