6.7 KiB
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 a200response in the standard{data, meta}envelope.GETreturns 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, returning200.PUTvalidates known preference keys: rejects invalidthemevalues, rejectslanguagevalues exceeding 10 characters.PUTwith invalid input returns400 Bad Requestwith 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_idin the path must match the authenticated user's ID), returning403 Forbiddenotherwise. - Preferences are persisted to PostgreSQL and survive service restarts.
- The database schema uses an upsert pattern —
PUTto a newuser_idcreates the record,PUTto an existinguser_idreplaces 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 viaauth.GetUser(ctx). - Response format: All responses use
httpresponse.OK/Created/NoContenthelpers for the{data, meta}envelope. - Error handling: Domain errors mapped to HTTP errors in handlers via
mapDomainError()pattern. Usehttperror.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_URLenv 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
- Authorization model: Should the authenticated user's ID come from the JWT
subclaim, or is there a separateuser_idfield in the token? (Needs alignment withauth.GetUser()contract.) - Unknown preference keys: Should unknown keys be silently accepted and stored, or rejected with a validation error? (Spec currently assumes they are preserved.)
- Preference size limit: Should there be a maximum size for the preferences JSON document to prevent abuse? (e.g., 10KB limit.)