User Preferences & Overrides System
The Axiom Genesis user preferences system implements cascading configuration where user settings automatically override tenant and system defaults. This allows each user to customize their experience while maintaining consistent branding and standards across the application.
Preference Priority (highest to lowest):
- User Preferences (
usr_app_settings) - Individual user customizations - Tenant Preferences (
ten_app_settings) - Organization-wide settings - System Defaults - Built-in application defaults
User preferences are completely optional. If a user setting is not specified, the system falls back to tenant configuration, and then to system defaults.
Architecture Overview
Data Flow on Login
graph TD
A["User Login"] --> B["Signin Returns tenant_config"]
B --> C["Tenant Loader Initializes"]
C --> D["Load Tenant Data & Preferences"]
D --> E["User Preferences Loader"]
E --> F["Fetch usr_app_settings via Entity Routes"]
F --> G["Deep Merge: User + Tenant + System"]
G --> H["Application Service Applies Changes"]
H --> I["CSS Variables & DOM Attributes Updated"]
I --> J["Redux Store Updated"]
J --> K["Components Render with Merged Preferences"]
Key Services
User Preferences Loader Service
Location: src/services/user-preferences-loader.service.ts
Handles loading and merging user preferences with tenant settings.
Key Methods:
| Method | Purpose |
|---|---|
loadUserPreferences(userId, tenantId, tenantPrefs) | Load user prefs and merge with tenant settings |
updateUserAppSettings(userId, tenantId, updates) | Update user settings and apply immediately |
getAppSettings() | Retrieve app settings category |
updateUserAppSettings() | Persist changes and re-apply |
Loading Flow:
- Fetches user record from
/entity/users/get_by_identity route - Parses
usr_app_settingsJSON column - Deep merges with tenant preferences (user wins)
- Calls Application Service to apply changes
- Updates Redux
appSettings.preferencesstore
User Preferences Application Service
Location: src/services/user-preferences-application.service.ts
Applies merged preferences to the DOM and CSS variables.
Key Method:
applyMergedPreferences(mergedPreferences: UserAppSettings): void
What Gets Applied:
- Theme Colors → 18 CSS variables (
--color-primary-main,--color-error, etc.) - Layout → Data attributes (
data-sidebar-position,data-sidebar-collapsed) - Typography → Font CSS variables
- Accessibility → Data attributes and multipliers
- Locale → localStorage persistence
Tenant Loader Service (Integration Point)
Location: src/services/tenant-loader.service.ts
Orchestrates loading of tenant and user data during signin.
Integration:
// After loading tenant data
await userPreferencesLoaderService.loadUserPreferences(
userId,
tenantId,
tenantPreferences,
);
User Preferences Schema
User preferences are stored in the database as JSON in the usr_app_settings column of the app_users table.
Complete Structure
- Full JSON Structure
- Theme Category
{
"theme": {
"mode": "light",
"light": {
"primary_color": "#8B5CF6",
"secondary_color": "#1E40AF",
"accent_color": "#EC4899",
"success_color": "#10B981",
"warning_color": "#F59E0B",
"error_color": "#EF4444",
"info_color": "#3B82F6",
"background_color": "#FFFFFF",
"surface_color": "#F9FAFB",
"text_primary": "#111827",
"text_secondary": "#6B7280",
"border_color": "#E5E7EB"
},
"dark": {
"primary_color": "#8B5CF6",
"secondary_color": "#1E40AF",
"accent_color": "#EC4899",
"success_color": "#10B981",
"warning_color": "#F59E0B",
"error_color": "#EF4444",
"info_color": "#3B82F6",
"background_color": "#111827",
"surface_color": "#1F2937",
"text_primary": "#F9FAFB",
"text_secondary": "#D1D5DB",
"border_color": "#374151"
}
},
"typography": {
"primary_font": "Segoe UI",
"secondary_font": "IBM Plex Mono",
"base_size": 14,
"line_height": 1.75
},
"layout": {
"sidebar_position": "RIGHT",
"sidebar_collapsed": false,
"compact_mode": false
},
"features": {
"show_search": true,
"show_notifications": true,
"enable_beta_features": true
},
"accessibility": {
"high_contrast": false,
"reduce_motion": false,
"screen_reader_friendly": true,
"font_size_multiplier": 1
},
"locale": {
"language": "en-US",
"timezone": "UTC",
"date_format": "MM/DD/YYYY",
"time_format": "12h",
"currency": "USD"
},
"notifications": {
"email": true,
"push": true,
"desktop": true
}
}
{
"mode": "light",
"light": {
"primary_color": "#8B5CF6",
"secondary_color": "#1E40AF",
"accent_color": "#EC4899",
"success_color": "#10B981",
"warning_color": "#F59E0B",
"error_color": "#EF4444",
"info_color": "#3B82F6",
"background_color": "#FFFFFF",
"surface_color": "#F9FAFB",
"text_primary": "#111827",
"text_secondary": "#6B7280",
"border_color": "#E5E7EB"
},
"dark": {
"primary_color": "#8B5CF6",
"secondary_color": "#1E40AF",
"accent_color": "#EC4899",
"success_color": "#10B981",
"warning_color": "#F59E0B",
"error_color": "#EF4444",
"info_color": "#3B82F6",
"background_color": "#111827",
"surface_color": "#1F2937",
"text_primary": "#F9FAFB",
"text_secondary": "#D1D5DB",
"border_color": "#374151"
}
}
Categories Explained
| Category | Purpose | Key Fields |
|---|---|---|
| theme | Color scheme and appearance | mode, light, dark colors |
| typography | Font and text settings | primary_font, base_size, line_height |
| layout | UI structure preferences | sidebar_position, sidebar_collapsed |
| features | Feature toggles | show_search, enable_beta_features |
| accessibility | Accessibility options | high_contrast, reduce_motion, multipliers |
| locale | Language and regional settings | language, timezone, date_format |
| notifications | Communication preferences | email, push, desktop |
Usage Examples
Component: Load User Preferences
- TypeScript
- JavaScript
import { useUserPreferences } from '@/hooks/useUserPreferences';
export function SettingsPanel() {
const { preferences, loading, error, updatePreferences } = useUserPreferences();
if (loading) return <div>Loading preferences...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Current Preferences</h2>
<p>Theme Mode: {preferences?.theme?.mode}</p>
<p>Sidebar Position: {preferences?.layout?.sidebar_position}</p>
</div>
);
}
import { useUserPreferences } from "@/hooks/useUserPreferences";
export function SettingsPanel() {
const { preferences, loading, error } = useUserPreferences();
if (loading) return <div>Loading preferences...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h2>Current Preferences</h2>
<p>Theme Mode: {preferences?.theme?.mode}</p>
<p>Sidebar Position: {preferences?.layout?.sidebar_position}</p>
</div>
);
}
Update Theme Preference
import { useUserPreferences } from '@/hooks/useUserPreferences';
export function ThemeSwitcher() {
const { updateTheme } = useUserPreferences();
const handleThemeChange = async (mode: 'light' | 'dark') => {
try {
await updateTheme({ mode });
// Preferences updated and applied immediately
console.log(`Theme changed to ${mode}`);
} catch (error) {
console.error('Failed to update theme:', error);
}
};
return (
<div className="theme-switcher">
<button onClick={() => handleThemeChange('light')}>☀️ Light</button>
<button onClick={() => handleThemeChange('dark')}>🌙 Dark</button>
</div>
);
}
Batch Update Multiple Settings
const { updatePreferences } = useUserPreferences();
await updatePreferences({
theme: {
mode: "dark",
dark: {
primary_color: "#2563EB",
},
},
layout: {
sidebar_collapsed: true,
compact_mode: true,
},
});
Access Preferences from Redux
import { useAppSelector } from '@/hooks/useRedux';
export function MyComponent() {
// Get merged preferences directly from Redux store
const preferences = useAppSelector(state => state.appSettings.preferences);
return (
<div
style={{
color: preferences?.theme?.[preferences.theme.mode]?.primary_color
}}
>
Title with User Theme Color
</div>
);
}
CSS Variables Reference
After user preferences are applied, these CSS variables are available globally.
Color Variables
/* Primary and Secondary */
--color-primary-main
--color-primary-light
--color-primary-dark
--color-secondary-main
--color-secondary-light
--color-secondary-dark
/* Status Colors */
--color-success
--color-warning
--color-error
--color-info
/* Neutral Colors */
--color-background
--color-surface
--color-text-primary
--color-text-secondary
--color-border
--color-header-bg
--color-footer-bg
--color-sidebar-bg
Typography Variables
--font-family-primary
--font-family-secondary
--font-size-base
--line-height
Layout Variables
--sidebar-width
--header-height
--footer-height
Accessibility Variables
--font-size-multiplier
DOM Data Attributes
Components can style based on applied preferences:
/* Theme Mode */
[data-theme="dark"] {
/* Dark mode styles */
}
[data-theme="light"] {
/* Light mode styles */
}
/* Layout */
[data-sidebar-position="LEFT"] {
/* Left sidebar */
}
[data-sidebar-position="RIGHT"] {
/* Right sidebar */
}
[data-sidebar-collapsed="true"] {
/* Collapsed sidebar */
}
[data-compact-mode="true"] {
/* Compact layout */
}
/* Accessibility */
[data-high-contrast="true"] {
/* High contrast styles */
}
[data-reduce-motion="true"] {
/* Reduced animations */
}
Override Priority in Action
Example 1: Purple Theme on Blue Tenant
| Layer | Setting | Value |
|---|---|---|
| System Default | Primary Color | #38B44A (green) |
| Tenant (ten_app_settings) | Primary Color | #2563EB (blue) |
| User (usr_app_settings) | Primary Color | #8B5CF6 (purple) |
| ✅ Actual Applied | Primary Color | #8B5CF6 (purple) |
Result: User preference wins all layers
// User selects purple
await updateTheme({
light: { primary_color: "#8B5CF6" },
});
// CSS variable updated
// --color-primary-main = #8B5CF6
Example 2: Dark Mode User on Light Tenant
| Layer | Setting | Value |
|---|---|---|
| System | Theme Mode | light |
| Tenant | Theme Mode | light |
| User | Theme Mode | dark |
| ✅ Actual Applied | Theme Mode | dark |
Result: Dark theme applied with dark color palette
await updateTheme({ mode: "dark" });
// HTML data-theme="dark"
// Dark color variables applied
// CSS variables use dark palette
Example 3: Compact Layout User on Normal Tenant
| Layer | Setting | Value |
|---|---|---|
| System | Compact Mode | false |
| Tenant | Compact Mode | false |
| User | Compact Mode | true |
| ✅ Actual Applied | Compact Mode | true |
Result: Compact layout applied
await updateLayout({ compact_mode: true });
// HTML data-compact-mode="true"
// Components can trigger compact styles
Database Storage
Table and Column
-- User preferences stored in app_users table
SELECT
usr_id,
usr_email,
usr_app_settings
FROM app_users
WHERE usr_id = 1;
Sample JSON Content
The usr_app_settings column contains JSON matching the schema above.
Entity Route Access
Fetch user preferences:
GET /entity/users/get_by_id
?usr_id=1
&usr_tenant_id=7
Update user preferences:
PUT /entity/users/update
Content-Type: application/json
{
"usr_id": 1,
"usr_tenant_id": 7,
"usr_app_settings": "{\"theme\":{\"mode\":\"dark\"},\"layout\":{\"sidebar_position\":\"RIGHT\"}}"
}
When using entity routes to update usr_app_settings, the value must be a stringified JSON object, not a parsed object.
API Integration
Signin Flow
The signin endpoint returns tenant configuration:
{
"token": "eyJhbGc...",
"user_id": 1,
"tenant_id": 7,
"tenant_config": {
"app_settings": {
"theme": { "mode": "light", ... },
"layout": { ... }
},
"details": {
"company_name": "Acme Corp"
},
"credentials": { ... }
}
}
Loading Sequence
- Signin → Returns
tenant_config - Tenant Loader → Loads tenant data and calls user preferences loader
- User Preferences Loader → Fetches
usr_app_settingsvia entity route - Application Service → Applies merged preferences
- Redux → Stores merged config in
appSettings.preferences - Components → Can access via hook or selector
Advanced: Adding Custom Preferences
Step 1: Add to Schema
Edit middleware/config/user-schema.js:
USER_APP_SETTINGS_SCHEMA = {
// ... existing categories
my_custom_setting: {
description: "My custom user setting",
type: "object",
properties: {
option1: {
type: "boolean",
default: true,
},
option2: {
type: "string",
default: "value",
},
},
},
};
Step 2: Update Frontend Type
Edit src/services/user-preferences-loader.service.ts:
export interface UserAppSettings {
// ... existing
my_custom_setting?: {
option1?: boolean;
option2?: string;
};
}
Step 3: Use in Component
const { updatePreferences, preferences } = useUserPreferences();
// Read
const customValue = preferences?.my_custom_setting?.option1;
// Update
await updatePreferences({
my_custom_setting: {
option1: false,
option2: "new_value",
},
});
Performance Considerations
| Aspect | Details |
|---|---|
| Lazy Loading | User preferences load after tenant config during signin |
| Caching | Merged preferences stored in Redux, no repeated API calls |
| Batch Updates | updatePreferences() deep merges, only changed fields updated |
| DOM Updates | CSS variables applied once at root, inherited by all children |
| Entity Routes | Cached by recordService, minimal database hits |
Troubleshooting
User Preferences Not Applying
Check 1: Browser console during login
// Should see no errors related to user preferences loading
Check 2: Database content
SELECT usr_id, usr_app_settings
FROM app_users
WHERE usr_id = 1;
-- usr_app_settings should contain valid JSON
Check 3: Redux DevTools
// Open Redux DevTools and check:
// state.appSettings.preferences should have merged config
Check 4: CSS Variables
// In browser console:
getComputedStyle(document.documentElement).getPropertyValue(
"--color-primary-main",
);
// Should return a color value, not empty
Tenant Preferences Overriding User Preferences
Cause: User loader not called or user app_settings empty
Solution:
- Verify tenant-loader.service calls userPreferencesLoaderService
- Populate usr_app_settings via entity route update
- Check browser network tab for entity route request
CSS Variables Not Updating
Cause: userPreferencesApplicationService not called
Solution:
- Check browser console for application service errors
- Verify CSS variables are set on
document.documentElement - Ensure no competing CSS rules with
!importantflag
Best Practices
Follow these practices when working with user preferences:
- Always Use Hook - Use
useUserPreferences()instead of manual API calls - Convenience Methods - Use
updateTheme(),updateLayout()instead ofupdatePreferences() - CSS Variables - Prefer CSS variables over inline styles for theming
- Graceful Fallback - User preferences are optional; always have fallbacks
- Redux Access - For simple reads, use Redux selector instead of hook
- Document New Settings - Update schema and this guide when adding preferences
Related Documentation
- Theme System - How theming works in Axiom Genesis
- Tenant Configuration - Tenant-level settings and configuration
- Frontend Architecture - Redux and state management
- Entity Routes - How entity routes work