User Preferences System
The User Preferences System provides a flexible, config-driven approach to managing user-specific settings and behavioral data in DeployStack. This system handles everything from onboarding states to UI preferences without requiring database migrations for new preferences.
System Overview
We use a separate table architecture where each preference is stored as an individual row, providing excellent queryability and performance. The system is designed around three core principles:
- Config-Driven: New preferences require only configuration changes, no database migrations
- Type-Safe: Full TypeScript integration with runtime validation
- Self-Service: Users manage only their own preferences with proper security isolation
Architecture Components
Configuration Layer
All preferences are defined in /src/config/user-preferences.ts
as the single source of truth. This file contains default values and type definitions for all available preferences.
Service Layer
The UserPreferencesService
handles all database operations, type conversion, and business logic for preference management.
API Layer
RESTful endpoints provide secure access to preferences with permission-based authorization.
Database Layer
The userPreferences
table stores individual key-value pairs with proper indexing for performance.
Adding New Preferences
Adding a new preference is remarkably simple and requires no database migrations.
Step 1: Update Configuration
Edit /src/config/user-preferences.ts
and add your new preference to the DEFAULT_USER_PREFERENCES
object:
export const DEFAULT_USER_PREFERENCES = {
// Existing preferences...
show_survey_overall: true,
show_survey_company: true,
// Your new preferences
new_feature_enabled: false,
user_language: 'en',
dashboard_layout: 'grid',
notification_frequency: 'daily',
} as const;
Step 2: Restart Application
That's it! Restart the application and:
- New users automatically receive the new preferences with default values
- Existing users can access the new preferences (they'll get defaults when first accessed)
- No database migration required
- API schemas and TypeScript types are automatically updated from your config changes
Single Source of Truth
The system automatically generates API validation schemas and TypeScript interfaces from the configuration file. This means:
- No duplication: You only define preferences in one place
- Completely generic: Works with any preference type (string, boolean, number)
- No special cases: All preferences are handled uniformly, no hardcoded values
- Automatic validation: API endpoints automatically validate against your config
- Type safety: TypeScript interfaces are auto-generated from your preferences
- Zero maintenance: Adding/removing preferences doesn't require schema updates
Using the Service Layer
The UserPreferencesService
provides a clean interface for working with preferences in your application code.
Basic Operations
import { UserPreferencesService } from '../services/UserPreferencesService';
import { getDb } from '../db';
const db = getDb();
const preferencesService = new UserPreferencesService(db);
// Get a specific preference with fallback default
const theme = await preferencesService.getPreference(userId, 'theme', 'auto');
// Set a single preference
await preferencesService.setPreference(userId, 'show_survey_overall', false);
// Update multiple preferences at once
await preferencesService.updatePreferences(userId, {
theme: 'dark',
sidebar_collapsed: true,
email_notifications_enabled: false
});
// Get all user preferences
const allPreferences = await preferencesService.getUserPreferences(userId);
Specialized Methods
// Walkthrough management
const shouldShow = await preferencesService.shouldShowWalkthrough(userId);
await preferencesService.completeWalkthrough(userId);
await preferencesService.cancelWalkthrough(userId);
// Notification acknowledgments
await preferencesService.acknowledgeNotification(userId, 'welcome-2024');
Integration in Route Handlers
export default async function myRoute(server: FastifyInstance) {
server.get('/my-feature', {
preValidation: requirePermission('preferences.view'),
}, async (request, reply) => {
const userId = request.user!.id;
const db = getDb();
const preferencesService = new UserPreferencesService(db);
// Check if user has enabled the feature
const featureEnabled = await preferencesService.getPreference(
userId,
'new_feature_enabled',
false
);
if (!featureEnabled) {
return reply.status(403).send({ error: 'Feature not enabled' });
}
// Continue with feature logic...
});
}
Security Model
The User Preferences System implements a strict self-service security model with important restrictions:
Core Security Principle: Self-Service Only
- Users can ONLY view and modify their own preferences
- Even
global_admin
users CANNOT access other users' preferences - No admin override capability exists (by design for privacy)
- All preference operations are strictly user-scoped
Permissions Required
preferences.view
- Required to read own preferencespreferences.edit
- Required to modify own preferences
Role Assignments
global_admin
- Has both view and edit permissions (for their own preferences only)global_user
- Has both view and edit permissions (for their own preferences only)- Team roles - No preference permissions (preferences are user-scoped, not team-scoped)
Access Control Implementation
- All routes extract
userId
from the authenticated user's session (request.user!.id
) - No route accepts a
userId
parameter - it's always the authenticated user - Permission-based authorization happens before validation (security-first pattern)
- Database queries are automatically scoped to the authenticated user's ID
What This Means for Developers
- You cannot build admin tools to manage other users' preferences
- Support teams cannot directly modify user preferences through the API
- All preference management is strictly self-service
- Users have complete privacy and control over their preference data
Current Available Preferences
The system currently supports these preference categories:
Survey Preferences
show_survey_overall
(boolean) - Display overall satisfaction surveyshow_survey_company
(boolean) - Display company-specific survey
Walkthrough Preferences
walkthrough_completed
(boolean) - User completed onboarding walkthroughwalkthrough_cancelled
(boolean) - User cancelled onboarding walkthrough
UI Preferences
theme
(string) - UI theme: 'light', 'dark', or 'auto'sidebar_collapsed
(boolean) - Sidebar collapsed state
Notification Preferences
email_notifications_enabled
(boolean) - Email notifications enabledbrowser_notifications_enabled
(boolean) - Browser notifications enablednotification_acknowledgments
(string) - Comma-separated acknowledged notification IDs
Feature Preferences
beta_features_enabled
(boolean) - Beta features enabled
Frontend Integration
The User Preferences System integrates seamlessly with frontend applications through the service layer and existing API endpoints. Frontend developers can access preferences through the established API patterns used throughout DeployStack.
Performance Considerations
The system is optimized for performance with several key features:
Database Indexing
- Primary index on
user_id
for fast user lookups - Composite index on
user_id, preference_key
for specific preference queries - Index on
preference_key
for analytics queries - Index on
updated_at
for temporal queries
Efficient Queries
- Single query to fetch all user preferences
- Batch updates for multiple preference changes
- Automatic type conversion between strings and native types
Type Safety Features
The system provides comprehensive type safety:
Runtime Validation
All preference values are validated against the configuration schema before storage.
TypeScript Integration
Full TypeScript support with auto-completion and type checking:
// Type-safe preference access
const preferences: UserPreferences = await preferencesService.getUserPreferences(userId);
// TypeScript knows the available keys and their types
const theme: string = preferences.theme || 'auto';
const sidebarCollapsed: boolean = preferences.sidebar_collapsed || false;
Configuration-Driven Types
Types are automatically derived from the configuration, ensuring consistency between defaults and usage.
Migration Strategy
If you need to rename or remove preferences:
Renaming Preferences
- Add the new preference to config with desired default
- Create a migration script to copy old values to new keys
- Remove the old preference from config after migration
- Restart application
Removing Preferences
- Remove from configuration file
- Restart application
- Old preference data remains in database but becomes inaccessible
- Optionally clean up old data with a maintenance script
Development Workflow
Local Development
- Add preference to config file
- Restart development server
- Test with new user registration (gets defaults automatically)
- Test with existing users (gets defaults on first access)
Production Deployment
- Deploy code changes with new preferences in config
- Restart application servers
- New preferences are immediately available
- No database downtime or migration required
Related Documentation
- API Security - Security patterns and authorization
- Role Management - Permission system details
- Database Schema - Complete database schema reference
Key Benefits
For Developers
- Zero Migration Workflow: Add preferences without database changes
- Type Safety: Full TypeScript support with runtime validation
- Simple API: Intuitive service layer methods
- Performance: Optimized queries with proper indexing
For Operations
- No Downtime: Preference additions require no database migrations
- Secure: Permission-based access with proper isolation
- Scalable: Separate table architecture supports complex queries
- Maintainable: Config-driven approach with clear separation of concerns
The User Preferences System represents a modern approach to user settings management, balancing flexibility with performance while maintaining the simplicity that DeployStack developers expect.
Backend End-to-End Testing
Complete E2E testing setup with Jest, Supertest, and automated database cleanup for DeployStack Backend development.
Gateway Development
Developer documentation for the DeployStack Gateway - the local secure proxy that manages MCP servers and credentials for enterprise teams.