slack5-1770541397/.sdlc/features/user-preferences/spec.md
rdev-worker 257ad77471
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /spec-feature user-preferences --requirements 'CRUD API for user pref...
2026-02-08 09:08:39 +00:00

6.7 KiB

Feature: User Preferences API

Problem Statement

Users need the ability to persist and retrieve personal preferences (theme, language, notification settings) so that their experience is consistent across sessions and devices. Currently, the preferences-api service exists as scaffolding with only an example CRUD resource — there is no actual preferences domain model or persistence.

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 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 API call so that I can initialize the UI efficiently.
  • As a backend service, I want to read a user's preferences to personalize behavior (e.g., notification delivery channel).

API Design

Endpoints

Method Path Auth Description
GET /api/preferences-api/preferences/{user_id} Required Get all preferences for a user
PUT /api/preferences-api/preferences/{user_id} Required Create or replace all preferences for a user

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

{
  "preferences": {
    "theme": "dark",
    "language": "en",
    "notifications": {
      "email": true,
      "push": true,
      "sms": false
    }
  }
}

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

{
  "data": {
    "user_id": "usr_abc123",
    "preferences": {
      "theme": "dark",
      "language": "en",
      "notifications": {
        "email": true,
        "push": true,
        "sms": false
      }
    },
    "updated_at": "2026-02-08T10:30:00Z"
  },
  "meta": {
    "request_id": "req_xyz",
    "timestamp": "2026-02-08T10:30:01Z"
  }
}

Storage Model

Preferences are stored as a JSON document per user (not individual key-value rows). This simplifies reads (single query) and writes (single upsert), and avoids N+1 patterns for users with many preferences.

Known Preference Keys

Key Type Default Validation
theme string "system" One of: "light", "dark", "system"
language string "en" BCP-47 language tag, max 10 chars
notifications.email bool true
notifications.push bool true
notifications.sms bool false

The schema should be extensible — unknown keys are preserved but not validated, allowing new preference keys to be introduced without schema migrations.

Acceptance Criteria

  • GET /api/preferences-api/preferences/{user_id} returns the user's preferences with a 200 response in the standard {data, meta} envelope.
  • GET returns default preferences (theme: "system", language: "en", notifications: {email: true, push: true, sms: false}) when no preferences have been saved for the user.
  • PUT /api/preferences-api/preferences/{user_id} creates or fully replaces the user's preferences, returning 200.
  • PUT validates known preference keys: rejects invalid theme values, rejects language values exceeding 10 characters.
  • PUT with invalid input returns 400 Bad Request with a descriptive error in the standard error envelope.
  • Both endpoints require authentication via auth.Middleware().
  • The authenticated user can only access their own preferences (the user_id in the path must match the authenticated user's ID), returning 403 Forbidden otherwise.
  • Preferences are persisted to PostgreSQL and survive service restarts.
  • The database schema uses an upsert pattern — PUT to a new user_id creates the record, PUT to an existing user_id replaces it.
  • OpenAPI spec is updated with both endpoints, request/response schemas, and examples.
  • Handler, service, and domain layers follow existing hexagonal architecture patterns.
  • Unit tests cover: handler request/response mapping, service business logic (defaults, validation, authorization), domain validation rules.
  • URL parameters use brace syntax {user_id} (not colon syntax).

Technical Constraints

  • Architecture: Must follow the existing hexagonal architecture (domain → service → port → adapter) established in the preferences-api service.
  • Database: PostgreSQL via pkg/database. Migrations embedded with //go:embed. Single table with JSONB column for preferences.
  • Auth: Endpoints protected by auth.Middleware(). User ID extracted from JWT claims via auth.GetUser(ctx).
  • Response format: All responses use httpresponse.OK/Created/NoContent helpers for the {data, meta} envelope.
  • Error handling: Domain errors mapped to HTTP errors in handlers via mapDomainError() pattern. Use httperror.BadRequest, httperror.NotFound, httperror.Forbidden.
  • Request binding: Use app.BindAndValidate() for PUT request body.
  • Router: chi router with {param} brace syntax for URL parameters.
  • Port: Service runs on port 8001 (already configured).

Dependencies

  • pkg/database — PostgreSQL connection and migration support (already exists).
  • pkg/auth — JWT middleware and user extraction (already exists).
  • pkg/app, pkg/httperror, pkg/httpresponse, pkg/httpvalidation — HTTP framework (already exists).
  • A running PostgreSQL instance for persistence (managed via DATABASE_URL env var).

Out of Scope

  • Bulk preferences for multiple users — Only single-user GET/PUT in this iteration.
  • PATCH (partial update) — Full replacement via PUT only. Partial updates may be added later.
  • DELETE endpoint — No need to delete preferences (they reset to defaults if the row is removed manually).
  • Preference history/audit log — No tracking of preference changes over time.
  • Admin override — No admin endpoint to modify another user's preferences.
  • Frontend integration — API only; frontend changes are a separate feature.
  • Rate limiting — Handled at the infrastructure layer, not in this feature.

Open Questions

  1. Authorization model: Should the authenticated user's ID come from the JWT sub claim, or is there a separate user_id field in the token? (Needs alignment with auth.GetUser() contract.)
  2. Unknown preference keys: Should unknown keys be silently accepted and stored, or rejected with a validation error? (Spec currently assumes they are preserved.)
  3. Preference size limit: Should there be a maximum size for the preferences JSON document to prevent abuse? (e.g., 10KB limit.)