slack5-1770529463/.sdlc/features/user-preferences/spec.md
rdev-worker 208033482e
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
2026-02-08 05:52:34 +00:00

6.6 KiB

Feature: User Preferences API

Problem Statement

Users of the platform need a way to persist and retrieve their personal preferences (theme, language, notification settings) across sessions and devices. Currently the preferences-api service exists as a scaffold with only example CRUD endpoints and an in-memory store. There is no real preferences domain, no database persistence, and no API for managing user preferences.

Application frontends need a reliable backend API to read and write per-user preference key-value pairs so that UI settings survive page refreshes, device switches, and service restarts.

User Stories

  • As a frontend application, I want to GET a user's preferences so that I can render the UI with their chosen theme, language, and notification settings.
  • As a frontend application, I want to PUT (upsert) a user's preferences so that changes to settings are persisted immediately.
  • As a platform operator, I want preferences stored in PostgreSQL so that they survive service restarts and are backed up with the rest of the data.
  • As a developer, I want default preference values defined server-side so that new users get sensible defaults without client-side logic.

Acceptance Criteria

  • GET /api/preferences-api/preferences/{user_id} returns all preferences for the given user as a JSON object
  • GET for a user with no stored preferences returns server-defined defaults (not 404)
  • PUT /api/preferences-api/preferences/{user_id} creates or updates preferences for the given user (full replace of provided keys)
  • PUT is idempotent -- calling it twice with the same body produces the same result
  • Preferences are stored as key-value pairs in PostgreSQL
  • The following preference keys are supported with validation:
    • theme -- string, one of: light, dark, system (default: system)
    • language -- string, BCP 47 language tag, validated format (default: en)
    • notifications_enabled -- boolean (default: true)
  • Unknown preference keys in a PUT request are rejected with 400 Bad Request
  • user_id path parameter is validated as a non-empty string (UUID format)
  • All responses use the standard {data, meta} envelope
  • OpenAPI spec is updated with the new endpoints and schemas
  • Database migration creates the user_preferences table
  • Existing example CRUD endpoints and domain are removed (replaced by preferences)
  • Handler, service, domain, port, and adapter layers follow hexagonal architecture
  • Unit tests cover service logic (defaults, validation, upsert behavior)
  • Handler tests cover HTTP layer (request binding, error responses, status codes)

Technical Constraints

  • Database: PostgreSQL via pkg/database (sqlx). Migration files in services/preferences-api/migrations/.
  • Port: Service runs on port 8001 (already configured).
  • URL routing: Must use brace syntax {user_id} (chi router). Never colon syntax.
  • Error handling: Domain errors mapped to HTTP errors via httperror.*. Handlers return error, wrapped with app.Wrap().
  • Request binding: Use app.BindAndValidate() for PUT request body.
  • Response format: Use httpresponse.OK() / httpresponse.NoContent() for responses.
  • Auth: Auth middleware is opt-in via config. Endpoints should be in an auth-protectable route group, but auth enforcement is not required for this feature (configurable via AUTH_ENABLED).
  • Preference storage model: Each preference is a row in the user_preferences table with columns: user_id, key, value, created_at, updated_at. This allows adding new preference keys without schema changes.
  • Defaults: When a stored preference is missing, the API merges server-defined defaults so the response always contains all known keys.

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
    }
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}

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

Request body:

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

Only provided keys are updated. Omitted keys retain their current value (or default if never set).

Response 200:

{
  "data": {
    "user_id": "550e8400-e29b-41d4-a716-446655440000",
    "preferences": {
      "theme": "dark",
      "language": "fr",
      "notifications_enabled": true
    }
  },
  "meta": {
    "request_id": "...",
    "timestamp": "..."
  }
}

Error 400 (unknown key):

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "unknown preference key: font_size"
  },
  "meta": { "..." }
}

Database Schema

CREATE TABLE user_preferences (
    user_id     UUID NOT NULL,
    key         VARCHAR(64) NOT NULL,
    value       TEXT NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (user_id, key)
);

CREATE INDEX idx_user_preferences_user_id ON user_preferences (user_id);

Dependencies

  • PostgreSQL database accessible via DATABASE_URL environment variable
  • pkg/database for connection pooling and migrations
  • pkg/app, pkg/httperror, pkg/httpresponse, pkg/httpvalidation for HTTP layer
  • pkg/auth for optional authentication middleware

Out of Scope

  • Per-preference-key access control (all preferences for a user are readable/writable as a unit)
  • DELETE endpoint for individual preferences (not in requirements)
  • Preference history / audit log
  • Bulk operations across multiple users
  • WebSocket push for real-time preference sync
  • Admin endpoints for managing preference definitions
  • Frontend integration (separate feature)

Open Questions

  1. Authorization model: Should the API enforce that user_id in the path matches the authenticated user's ID? Or is cross-user preference access allowed (e.g., for admin tools)? Current assumption: no enforcement, auth is opt-in via config.
  2. Preference value types: Should values be typed (string/bool/number) at the API level, or stored/returned as strings with client-side parsing? Current assumption: typed in API response (theme as string, notifications_enabled as boolean), stored as TEXT in DB with serialization.
  3. Partial vs full update semantics: PUT currently does partial update (merge). Should it be full replace (all keys must be provided)? Current assumption: partial merge -- only provided keys are updated, missing keys retain current values.