Skip to content

Commit ce24a0d

Browse files
committed
feat(docs): add comprehensive guide for User Preferences System, covering architecture, management, and security
1 parent 5e689b4 commit ce24a0d

File tree

3 files changed

+404
-9
lines changed

3 files changed

+404
-9
lines changed

docs/development/backend/oauth2-server.mdx

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,7 @@ For complete API specifications including request parameters, response schemas,
154154

155155
### Available Scopes
156156

157-
Fine-grained permissions for API access:
158-
159-
- `mcp:read` - Read MCP server configurations
160-
- `account:read` - Read account information
161-
- `user:read` - Read user profile
162-
- `teams:read` - Read team memberships
163-
- `offline_access` - Refresh token support
157+
For the current list of supported scopes, see the source code at `services/backend/src/services/oauth/authorizationService.ts` in the `validateScope()` method.
164158

165159
### Scope Enforcement
166160

@@ -321,6 +315,75 @@ Comprehensive logging for debugging:
321315
- Error conditions
322316
- Security events
323317

318+
## OAuth Scope Management
319+
320+
The backend validates OAuth scopes to control API access. Scope configuration must stay synchronized between the backend and gateway.
321+
322+
### Current Scopes
323+
324+
For the current list of supported scopes, check the source code at:
325+
- **Backend validation**: `services/backend/src/services/oauth/authorizationService.ts` in the `validateScope()` method
326+
- **Gateway requests**: `services/gateway/src/utils/auth-config.ts` in the `scopes` array
327+
328+
### Adding New Scopes
329+
330+
When adding support for a new OAuth scope in the backend:
331+
332+
1. **Add the scope** to the `allowedScopes` array in `services/backend/src/services/oauth/authorizationService.ts`
333+
2. **Update the gateway** to request the new scope (see [Gateway OAuth Implementation](/development/gateway/oauth))
334+
3. **Apply scope enforcement** to relevant API endpoints using middleware
335+
4. **Test the complete flow** to ensure proper scope validation
336+
337+
Example:
338+
```typescript
339+
// In services/backend/src/services/oauth/authorizationService.ts
340+
static validateScope(scope: string): boolean {
341+
const requestedScopes = scope.split(' ');
342+
const allowedScopes = [
343+
'mcp:read',
344+
'mcp:categories:read',
345+
'your-new-scope', // Add new scope here
346+
// ... other scopes
347+
];
348+
349+
return requestedScopes.every(s => allowedScopes.includes(s));
350+
}
351+
```
352+
353+
### Scope Enforcement in Routes
354+
355+
Apply scope requirements to API endpoints:
356+
357+
```typescript
358+
// Single scope requirement
359+
server.get('/api/your-endpoint', {
360+
preValidation: [
361+
requireValidAccessToken(),
362+
requireOAuthScope('your-new-scope')
363+
]
364+
}, async (request, reply) => {
365+
// Your endpoint logic
366+
});
367+
368+
// Multiple scope options
369+
server.get('/api/another-endpoint', {
370+
preValidation: [
371+
requireValidAccessToken(),
372+
requireAnyOAuthScope(['scope1', 'scope2'])
373+
]
374+
}, async (request, reply) => {
375+
// Your endpoint logic
376+
});
377+
```
378+
379+
### Scope Synchronization
380+
381+
**Critical**: The backend and gateway must have matching scope configurations:
382+
- If backend supports a scope but gateway doesn't request it, users won't get that permission
383+
- If gateway requests a scope but backend doesn't support it, authentication will fail
384+
385+
Always coordinate scope changes between both services.
386+
324387
## Gateway Integration
325388

326389
The OAuth2 server integrates with the DeployStack Gateway:
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
---
2+
title: User Preferences System
3+
description: Developer guide for managing user preferences in DeployStack Backend - adding new preferences, using the service layer, and understanding the architecture.
4+
---
5+
6+
# User Preferences System
7+
8+
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.
9+
10+
## System Overview
11+
12+
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:
13+
14+
- **Config-Driven**: New preferences require only configuration changes, no database migrations
15+
- **Type-Safe**: Full TypeScript integration with runtime validation
16+
- **Self-Service**: Users manage only their own preferences with proper security isolation
17+
18+
## Architecture Components
19+
20+
### Configuration Layer
21+
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.
22+
23+
### Service Layer
24+
The `UserPreferencesService` handles all database operations, type conversion, and business logic for preference management.
25+
26+
### API Layer
27+
RESTful endpoints provide secure access to preferences with permission-based authorization.
28+
29+
### Database Layer
30+
The `userPreferences` table stores individual key-value pairs with proper indexing for performance.
31+
32+
## Adding New Preferences
33+
34+
Adding a new preference is remarkably simple and requires no database migrations.
35+
36+
### Step 1: Update Configuration
37+
38+
Edit `/src/config/user-preferences.ts` and add your new preference to the `DEFAULT_USER_PREFERENCES` object:
39+
40+
```typescript
41+
export const DEFAULT_USER_PREFERENCES = {
42+
// Existing preferences...
43+
show_survey_overall: true,
44+
show_survey_company: true,
45+
46+
// Your new preferences
47+
new_feature_enabled: false,
48+
user_language: 'en',
49+
dashboard_layout: 'grid',
50+
notification_frequency: 'daily',
51+
} as const;
52+
```
53+
54+
### Step 2: Restart Application
55+
56+
That's it! Restart the application and:
57+
- New users automatically receive the new preferences with default values
58+
- Existing users can access the new preferences (they'll get defaults when first accessed)
59+
- No database migration required
60+
- **API schemas and TypeScript types are automatically updated** from your config changes
61+
62+
### Single Source of Truth
63+
64+
The system automatically generates API validation schemas and TypeScript interfaces from the configuration file. This means:
65+
66+
- **No duplication**: You only define preferences in one place
67+
- **Completely generic**: Works with any preference type (string, boolean, number)
68+
- **No special cases**: All preferences are handled uniformly, no hardcoded values
69+
- **Automatic validation**: API endpoints automatically validate against your config
70+
- **Type safety**: TypeScript interfaces are auto-generated from your preferences
71+
- **Zero maintenance**: Adding/removing preferences doesn't require schema updates
72+
73+
## Using the Service Layer
74+
75+
The `UserPreferencesService` provides a clean interface for working with preferences in your application code.
76+
77+
### Basic Operations
78+
79+
```typescript
80+
import { UserPreferencesService } from '../services/UserPreferencesService';
81+
import { getDb } from '../db';
82+
83+
const db = getDb();
84+
const preferencesService = new UserPreferencesService(db);
85+
86+
// Get a specific preference with fallback default
87+
const theme = await preferencesService.getPreference(userId, 'theme', 'auto');
88+
89+
// Set a single preference
90+
await preferencesService.setPreference(userId, 'show_survey_overall', false);
91+
92+
// Update multiple preferences at once
93+
await preferencesService.updatePreferences(userId, {
94+
theme: 'dark',
95+
sidebar_collapsed: true,
96+
email_notifications_enabled: false
97+
});
98+
99+
// Get all user preferences
100+
const allPreferences = await preferencesService.getUserPreferences(userId);
101+
```
102+
103+
### Specialized Methods
104+
105+
```typescript
106+
// Walkthrough management
107+
const shouldShow = await preferencesService.shouldShowWalkthrough(userId);
108+
await preferencesService.completeWalkthrough(userId);
109+
await preferencesService.cancelWalkthrough(userId);
110+
111+
// Notification acknowledgments
112+
await preferencesService.acknowledgeNotification(userId, 'welcome-2024');
113+
```
114+
115+
### Integration in Route Handlers
116+
117+
```typescript
118+
export default async function myRoute(server: FastifyInstance) {
119+
server.get('/my-feature', {
120+
preValidation: requirePermission('preferences.view'),
121+
}, async (request, reply) => {
122+
const userId = request.user!.id;
123+
const db = getDb();
124+
const preferencesService = new UserPreferencesService(db);
125+
126+
// Check if user has enabled the feature
127+
const featureEnabled = await preferencesService.getPreference(
128+
userId,
129+
'new_feature_enabled',
130+
false
131+
);
132+
133+
if (!featureEnabled) {
134+
return reply.status(403).send({ error: 'Feature not enabled' });
135+
}
136+
137+
// Continue with feature logic...
138+
});
139+
}
140+
```
141+
142+
## Security Model
143+
144+
The User Preferences System implements a strict self-service security model with important restrictions:
145+
146+
### Core Security Principle: Self-Service Only
147+
- **Users can ONLY view and modify their own preferences**
148+
- **Even `global_admin` users CANNOT access other users' preferences**
149+
- **No admin override capability exists (by design for privacy)**
150+
- **All preference operations are strictly user-scoped**
151+
152+
### Permissions Required
153+
- `preferences.view` - Required to read own preferences
154+
- `preferences.edit` - Required to modify own preferences
155+
156+
### Role Assignments
157+
- `global_admin` - Has both view and edit permissions (for their own preferences only)
158+
- `global_user` - Has both view and edit permissions (for their own preferences only)
159+
- Team roles - No preference permissions (preferences are user-scoped, not team-scoped)
160+
161+
### Access Control Implementation
162+
- All routes extract `userId` from the authenticated user's session (`request.user!.id`)
163+
- No route accepts a `userId` parameter - it's always the authenticated user
164+
- Permission-based authorization happens before validation (security-first pattern)
165+
- Database queries are automatically scoped to the authenticated user's ID
166+
167+
### What This Means for Developers
168+
- You cannot build admin tools to manage other users' preferences
169+
- Support teams cannot directly modify user preferences through the API
170+
- All preference management is strictly self-service
171+
- Users have complete privacy and control over their preference data
172+
173+
## Current Available Preferences
174+
175+
The system currently supports these preference categories:
176+
177+
### Survey Preferences
178+
- `show_survey_overall` (boolean) - Display overall satisfaction survey
179+
- `show_survey_company` (boolean) - Display company-specific survey
180+
181+
### Walkthrough Preferences
182+
- `walkthrough_completed` (boolean) - User completed onboarding walkthrough
183+
- `walkthrough_cancelled` (boolean) - User cancelled onboarding walkthrough
184+
185+
### UI Preferences
186+
- `theme` (string) - UI theme: 'light', 'dark', or 'auto'
187+
- `sidebar_collapsed` (boolean) - Sidebar collapsed state
188+
189+
### Notification Preferences
190+
- `email_notifications_enabled` (boolean) - Email notifications enabled
191+
- `browser_notifications_enabled` (boolean) - Browser notifications enabled
192+
- `notification_acknowledgments` (string) - Comma-separated acknowledged notification IDs
193+
194+
### Feature Preferences
195+
- `beta_features_enabled` (boolean) - Beta features enabled
196+
197+
## Frontend Integration
198+
199+
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.
200+
201+
## Performance Considerations
202+
203+
The system is optimized for performance with several key features:
204+
205+
### Database Indexing
206+
- Primary index on `user_id` for fast user lookups
207+
- Composite index on `user_id, preference_key` for specific preference queries
208+
- Index on `preference_key` for analytics queries
209+
- Index on `updated_at` for temporal queries
210+
211+
### Efficient Queries
212+
- Single query to fetch all user preferences
213+
- Batch updates for multiple preference changes
214+
- Automatic type conversion between strings and native types
215+
216+
## Type Safety Features
217+
218+
The system provides comprehensive type safety:
219+
220+
### Runtime Validation
221+
All preference values are validated against the configuration schema before storage.
222+
223+
### TypeScript Integration
224+
Full TypeScript support with auto-completion and type checking:
225+
226+
```typescript
227+
// Type-safe preference access
228+
const preferences: UserPreferences = await preferencesService.getUserPreferences(userId);
229+
230+
// TypeScript knows the available keys and their types
231+
const theme: string = preferences.theme || 'auto';
232+
const sidebarCollapsed: boolean = preferences.sidebar_collapsed || false;
233+
```
234+
235+
### Configuration-Driven Types
236+
Types are automatically derived from the configuration, ensuring consistency between defaults and usage.
237+
238+
## Migration Strategy
239+
240+
If you need to rename or remove preferences:
241+
242+
### Renaming Preferences
243+
1. Add the new preference to config with desired default
244+
2. Create a migration script to copy old values to new keys
245+
3. Remove the old preference from config after migration
246+
4. Restart application
247+
248+
### Removing Preferences
249+
1. Remove from configuration file
250+
2. Restart application
251+
3. Old preference data remains in database but becomes inaccessible
252+
4. Optionally clean up old data with a maintenance script
253+
254+
## Development Workflow
255+
256+
### Local Development
257+
1. Add preference to config file
258+
2. Restart development server
259+
3. Test with new user registration (gets defaults automatically)
260+
4. Test with existing users (gets defaults on first access)
261+
262+
### Production Deployment
263+
1. Deploy code changes with new preferences in config
264+
2. Restart application servers
265+
3. New preferences are immediately available
266+
4. No database downtime or migration required
267+
268+
## Related Documentation
269+
270+
- [API Security](/development/backend/api-security) - Security patterns and authorization
271+
- [Role Management](/development/backend/roles) - Permission system details
272+
- [Database Schema](https://github.com/deploystackio/deploystack/blob/main/services/backend/src/db/schema.sqlite.ts) - Complete database schema reference
273+
274+
## Key Benefits
275+
276+
### For Developers
277+
- **Zero Migration Workflow**: Add preferences without database changes
278+
- **Type Safety**: Full TypeScript support with runtime validation
279+
- **Simple API**: Intuitive service layer methods
280+
- **Performance**: Optimized queries with proper indexing
281+
282+
### For Operations
283+
- **No Downtime**: Preference additions require no database migrations
284+
- **Secure**: Permission-based access with proper isolation
285+
- **Scalable**: Separate table architecture supports complex queries
286+
- **Maintainable**: Config-driven approach with clear separation of concerns
287+
288+
The User Preferences System represents a modern approach to user settings management, balancing flexibility with performance while maintaining the simplicity that DeployStack developers expect.

0 commit comments

Comments
 (0)