Skip to main content

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):

  1. User Preferences (usr_app_settings) - Individual user customizations
  2. Tenant Preferences (ten_app_settings) - Organization-wide settings
  3. System Defaults - Built-in application defaults
info

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:

MethodPurpose
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:

  1. Fetches user record from /entity/users/get_by_id entity route
  2. Parses usr_app_settings JSON column
  3. Deep merges with tenant preferences (user wins)
  4. Calls Application Service to apply changes
  5. Updates Redux appSettings.preferences store

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

{
"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
}
}

Categories Explained

CategoryPurposeKey Fields
themeColor scheme and appearancemode, light, dark colors
typographyFont and text settingsprimary_font, base_size, line_height
layoutUI structure preferencessidebar_position, sidebar_collapsed
featuresFeature togglesshow_search, enable_beta_features
accessibilityAccessibility optionshigh_contrast, reduce_motion, multipliers
localeLanguage and regional settingslanguage, timezone, date_format
notificationsCommunication preferencesemail, push, desktop

Usage Examples

Component: Load User Preferences

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>
);
}

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

LayerSettingValue
System DefaultPrimary Color#38B44A (green)
Tenant (ten_app_settings)Primary Color#2563EB (blue)
User (usr_app_settings)Primary Color#8B5CF6 (purple)
✅ Actual AppliedPrimary 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

LayerSettingValue
SystemTheme Modelight
TenantTheme Modelight
UserTheme Modedark
✅ Actual AppliedTheme Modedark

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

LayerSettingValue
SystemCompact Modefalse
TenantCompact Modefalse
UserCompact Modetrue
✅ Actual AppliedCompact Modetrue

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\"}}"
}
warning

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

  1. Signin → Returns tenant_config
  2. Tenant Loader → Loads tenant data and calls user preferences loader
  3. User Preferences Loader → Fetches usr_app_settings via entity route
  4. Application Service → Applies merged preferences
  5. Redux → Stores merged config in appSettings.preferences
  6. 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

AspectDetails
Lazy LoadingUser preferences load after tenant config during signin
CachingMerged preferences stored in Redux, no repeated API calls
Batch UpdatesupdatePreferences() deep merges, only changed fields updated
DOM UpdatesCSS variables applied once at root, inherited by all children
Entity RoutesCached 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:

  1. Verify tenant-loader.service calls userPreferencesLoaderService
  2. Populate usr_app_settings via entity route update
  3. Check browser network tab for entity route request

CSS Variables Not Updating

Cause: userPreferencesApplicationService not called

Solution:

  1. Check browser console for application service errors
  2. Verify CSS variables are set on document.documentElement
  3. Ensure no competing CSS rules with !important flag

Best Practices

tip

Follow these practices when working with user preferences:

  • Always Use Hook - Use useUserPreferences() instead of manual API calls
  • Convenience Methods - Use updateTheme(), updateLayout() instead of updatePreferences()
  • 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