slack5-1770524327/.sdlc/features/user-preferences/spec.md
rdev-worker b941ed2b5c
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
2026-02-08 04:30:13 +00:00

6.0 KiB

Feature: User Preferences API

Problem Statement

Users of the platform have no way to persist their personal settings (theme, language, notification preferences). Without a preferences API, every client must manage settings locally, leading to inconsistent experiences across devices and sessions. The preferences-api service currently contains only scaffold/example code and needs to be replaced with real preference management functionality.

User Stories

  • As a user, I want to save my preferred theme so that the UI looks the same every time I log in.
  • As a user, I want to set my preferred language so that content is displayed in my language across devices.
  • As a user, I want to configure my notification settings so that I only receive the alerts I care about.
  • As a frontend developer, I want a simple GET/PUT API to read and write user preferences so that I can build settings pages without managing local storage.

Acceptance Criteria

  • GET /api/preferences-api/preferences/{user_id} returns all preferences for a user as key-value pairs
  • GET returns a default set of preferences (theme, language, notifications) when the user has no saved preferences
  • PUT /api/preferences-api/preferences/{user_id} creates or updates preferences for a user (upsert semantics)
  • PUT validates that theme is one of: light, dark, system
  • PUT validates that language is a valid BCP-47 language tag (e.g., en, fr, es, de, ja)
  • PUT validates that notifications is an object with boolean fields: email, push, in_app
  • PUT supports partial updates — omitted keys retain their current values
  • All responses follow the {data, meta} envelope pattern
  • Preferences are persisted in PostgreSQL (not in-memory)
  • Database migration creates the preferences table
  • user_id is validated as a UUID format
  • PUT on protected route group (requires auth when AUTH_ENABLED=true)
  • GET on protected route group (requires auth when AUTH_ENABLED=true)
  • Handler tests cover success, validation errors, not-found defaults, and invalid user ID
  • Service-layer tests cover business logic with mock repository
  • Domain model enforces preference value constraints
  • OpenAPI spec documents both endpoints with request/response schemas
  • Existing Example CRUD scaffold code is removed

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": {
        "email": true,
        "push": false,
        "in_app": true
      }
    },
    "updated_at": "2026-01-15T10:30:00Z"
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}

PUT /api/preferences-api/preferences/{user_id}

Request body (partial update):

{
  "theme": "dark",
  "notifications": {
    "email": false
  }
}

Response 200:

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

Default Preference Values

Key Default Value
theme system
language en
notifications.email true
notifications.push true
notifications.in_app true

Technical Constraints

  • Database: PostgreSQL via pkg/database (connection pool, migrations, transactions)
  • Architecture: Hexagonal — domain, service, port (interface), adapter (postgres implementation)
  • Router: chi with {param} brace syntax for URL parameters
  • Validation: pkg/httpvalidation with go-playground/validator struct tags
  • Error handling: Domain errors mapped to HTTP errors via httperror.* factories
  • Response format: All responses wrapped via httpresponse.OK, httpresponse.Created, etc.
  • Auth: Opt-in via auth.Middleware() in route group, controlled by AUTH_ENABLED env var
  • Migrations: SQL files in services/preferences-api/migrations/ using pkg/database.RunMigrations
  • Port: Service runs on port 8001 (existing allocation)

Database Schema

CREATE TABLE IF NOT EXISTS user_preferences (
    user_id       UUID PRIMARY KEY,
    theme         VARCHAR(10) NOT NULL DEFAULT 'system',
    language      VARCHAR(10) NOT NULL DEFAULT 'en',
    notif_email   BOOLEAN NOT NULL DEFAULT true,
    notif_push    BOOLEAN NOT NULL DEFAULT true,
    notif_in_app  BOOLEAN NOT NULL DEFAULT true,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Dependencies

  • PostgreSQL database available (connection string via DATABASE_URL env var)
  • pkg/database package for connection pooling and migrations
  • pkg/app, pkg/httperror, pkg/httpresponse, pkg/httpvalidation packages (all exist)
  • pkg/auth for JWT middleware (exists)

Out of Scope

  • User creation/management (user IDs come from an external auth system)
  • Preference history/audit log
  • Preference schemas beyond theme, language, notifications
  • Admin endpoints to manage preferences for other users
  • Real-time preference sync via WebSocket
  • Preference import/export

Open Questions

  1. Authorization model: Should GET /preferences/{user_id} be restricted so users can only read their own preferences, or can any authenticated user read any user's preferences? (Spec assumes auth-protected but no ownership check — simplest to start.)
  2. Extensible keys: Should the API support arbitrary preference keys beyond the three defined, or should it be a fixed schema? (Spec assumes fixed schema for type safety and validation.)
  3. Rate limiting: Should PUT be rate-limited to prevent abuse? (Spec assumes no — can be added later via middleware.)