slate-v3-1770514618/.sdlc/features/user-preferences/spec.md
rdev-worker a167ae7c25
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
2026-02-08 01:43:39 +00:00

6.8 KiB

Feature: User Preferences API

Problem Statement

Users need a way to persist and retrieve their application preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently there is no preferences storage — the preferences-api service exists as a skeleton with only the example CRUD scaffold. This feature replaces the example entity with a real user preferences system.

User Stories

  • As an authenticated user, I want to save my theme preference so that the UI renders in my chosen theme across sessions.
  • As an authenticated user, I want to save my language preference so that content is displayed in my preferred language.
  • As an authenticated user, I want to configure my notification settings so that I only receive the alerts I care about.
  • As an authenticated user, I want to retrieve all my preferences in a single call so that the frontend can initialize quickly.
  • As an API consumer, I want to update individual preference keys without overwriting unrelated settings.

API Design

Endpoints

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

Request / Response

GET /api/preferences-api/preferences/{user_id}

Response 200 OK:

{
  "data": {
    "user_id": "uuid",
    "preferences": {
      "theme": "dark",
      "language": "en",
      "notifications": {
        "email": true,
        "push": false,
        "digest": "weekly"
      }
    },
    "updated_at": "2026-02-08T00:00:00Z"
  },
  "meta": { "request_id": "...", "timestamp": "..." }
}

Response 404 Not Found (no preferences saved yet):

{
  "error": { "code": "NOT_FOUND", "message": "preferences not found" },
  "meta": { "request_id": "...", "timestamp": "..." }
}

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

Request body:

{
  "preferences": {
    "theme": "light",
    "notifications": {
      "email": false
    }
  }
}

Response 200 OK (returns merged result):

{
  "data": {
    "user_id": "uuid",
    "preferences": {
      "theme": "light",
      "language": "en",
      "notifications": {
        "email": false,
        "push": false,
        "digest": "weekly"
      }
    },
    "updated_at": "2026-02-08T00:00:01Z"
  },
  "meta": { "request_id": "...", "timestamp": "..." }
}

Merge Semantics

PUT performs a shallow merge at the top-level keys (theme, language, notifications). Nested objects like notifications are replaced entirely when provided. This keeps behavior predictable without requiring JSON Patch complexity.

Acceptance Criteria

  • GET /api/preferences-api/preferences/{user_id} returns 200 with stored preferences
  • GET returns 404 when no preferences exist for the user
  • PUT /api/preferences-api/preferences/{user_id} creates preferences if none exist (upsert)
  • PUT merges provided keys with existing preferences (shallow merge)
  • PUT returns 200 with the full merged preference set
  • PUT validates that preferences field is present and is a JSON object
  • PUT rejects unknown top-level preference keys with 400 Bad Request
  • Both endpoints require authentication via auth.Middleware()
  • Authenticated user can only access their own preferences (user_id in path must match token subject), unless they have an admin role
  • user_id path parameter is validated as a UUID
  • Preferences are persisted in-memory via the existing adapter pattern (database adapter deferred)
  • OpenAPI spec documents both endpoints with schemas, examples, and error responses
  • Domain model defines allowed preference keys and validation rules
  • Handler tests cover success paths, validation errors, auth failures, and not-found cases
  • Service tests cover merge logic, create-on-first-PUT, and authorization checks
  • All existing example scaffold code is removed and replaced with preferences code

Data Model

Domain Entity: Preference

type UserID string

type Preferences struct {
    UserID    UserID
    Theme     string           // "light", "dark", "system"
    Language  string           // ISO 639-1 code: "en", "es", "fr", etc.
    Notifications NotificationSettings
    UpdatedAt time.Time
}

type NotificationSettings struct {
    Email  bool
    Push   bool
    Digest string  // "daily", "weekly", "never"
}

Allowed Values

Key Type Allowed Values Default
theme string light, dark, system system
language string ISO 639-1 codes en
notifications.email bool true, false true
notifications.push bool true, false true
notifications.digest string daily, weekly, never weekly

Technical Constraints

  • Must follow the existing hexagonal architecture: domain → service → port → adapter
  • Must use app.Wrap(), app.BindAndValidate(), httpresponse.*, httperror.* patterns
  • Must use auth.Middleware() for protected routes
  • In-memory adapter for initial implementation (matches existing pattern); database migration deferred to a follow-up feature
  • Preference values must be validated against allowed values in the domain layer
  • OpenAPI spec must be updated to replace example endpoints with preference endpoints
  • Route base path remains /api/preferences-api

Dependencies

  • pkg/app — handler wrapping, request binding
  • pkg/auth — JWT middleware, context user extraction
  • pkg/httperror — typed HTTP errors
  • pkg/httpresponse — response envelope
  • pkg/httpvalidation — struct validation
  • pkg/openapi — spec builder
  • pkg/logging — structured logging
  • Existing preferences-api service skeleton (to be modified in-place)

Out of Scope

  • Database persistence (PostgreSQL adapter) — separate follow-up feature
  • Bulk preference operations across multiple users
  • Preference history / audit log
  • Preference defaults management API (defaults are hardcoded in domain)
  • Frontend integration (consuming the API from apps/)
  • Rate limiting
  • Preference change webhooks / event publishing

Open Questions

  1. Authorization model: Should any user be able to read another user's preferences, or is it strictly own-user-only? The spec assumes own-user + admin override, but this needs confirmation.
  2. Additional preference keys: Are theme, language, and notifications the complete set, or should the schema be extensible for future keys without code changes?
  3. Default preferences: When GET returns 404, should we instead return a 200 with default values? This simplifies the frontend but changes the semantic contract.