6.6 KiB
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 objectGETfor 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)PUTis 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_idpath 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_preferencestable - 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 inservices/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 returnerror, wrapped withapp.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_preferencestable 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_URLenvironment variable pkg/databasefor connection pooling and migrationspkg/app,pkg/httperror,pkg/httpresponse,pkg/httpvalidationfor HTTP layerpkg/authfor 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
- Authorization model: Should the API enforce that
user_idin 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. - 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.
- 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.