User Settings Hierarchy — Unified Preference System
Audiences: developer, internal
Dudoxx HMS resolves every configuration value — UI theme, LLM model, transcription provider, voice agent, TTS voice — through a three-layer cascade (User → Organization → Platform) so a clinic can define defaults while doctors retain personal overrides, and the system works out-of-the-box without any manual configuration.
Business Purpose
A clinical platform deployed across dozens of clinics must balance two competing needs: ops teams want standardized LLM/transcription defaults per clinic, while individual doctors need to choose their own AI models, audio devices, and display preferences. A flat per-user config breaks at scale (no org-level defaults); pure org-level config blocks individual customization.
The Dudoxx settings hierarchy solves this with a three-layer resolution chain that is transparent to callers: UserPreferences (Prisma row, user-specific overrides) → OrganizationSettings (Prisma row, clinic-level defaults) → platform defaults (env vars, read by UnifiedSettingsService). Every resolved settings object includes a _sources map that tells the frontend and AI tooling exactly which layer each field came from ("user", "org", or "system").
Audiences
- Investor: Enables per-clinic LLM/transcription customization without per-doctor onboarding cost — admin sets org defaults, doctors opt-in to overrides. Reduces support burden from configuration drift.
- Clinical buyer (doctor/nurse/receptionist): Doctors can choose their preferred AI model, transcription language, TTS voice, and display theme from the Settings panel. Changes take effect immediately without admin intervention. "Reset to defaults" rolls back to clinic defaults, not platform defaults.
- Developer/partner: Single
GET /settings/unifiedendpoint returns the fully resolved config for the authenticated user in one call. Layer source is reported in thesourceobject. Partial PATCH updates available per category:llm,transcriber,tts,voiceAgent,ui. - Internal (ops/support): Org admins update
OrganizationSettingsviaPATCH /settings/organizations/:orgId. Super admins control platform defaults via env vars. All settings mutations are audit-logged viaLoggingService.logAudit().
Architecture
The settings system spans three NestJS modules that collaborate through UnifiedSettingsService:
GET /settings/unified
└── SettingsService.getUnifiedSettings(userId, organizationId)
└── UnifiedSettingsService.getUserSettings(userId)
├── UserPreferences (prisma-main) — user row, opt-in overrides
├── OrganizationSettings (prisma-main) — org defaults (JSON blob)
└── Platform defaults — env vars (LITELLM_*, DEEPGRAM_*, etc.)
└── UnifiedSettingsService.getServiceEndpoints(userId)
└── service endpoint URLs from env config
GET /preferences (user-settings module)
└── UserSettingsService.getPreferences(userId)
├── prisma.userPreferences.findUnique({ userId })
└── UnifiedSettingsService.getUserSettings(userId) ← fills missing fields
UserSettingsService (in user-mgmt/user-settings/) is the per-user low-level read/write layer. SettingsService (in organization/settings/) is the org-aware orchestrator. Both use UnifiedSettingsService as the resolution engine.
Settings categories and their DB columns:
| Category | Fields | Storage |
|---|---|---|
| UI | theme, locale, fontSize, highContrast, reduceMotion, dateFormat, timeFormat, timezone, sidebarCollapsed, compactMode, defaultPageSize | UserPreferences (Prisma) |
| LLM | litellmReasoningModel, llmProvider, llmTemperature, llmMaxTokens, litellmBudget, litellmRpmLimit, litellmTpmLimit, aiSuggestionsEnabled, aiAutoSummarize | UserPreferences (Prisma) |
| Transcriber | defaultTranscriber, transcriberLanguage, transcriberModel, diarizeEnabled, punctuateEnabled, smartFormatEnabled, audioDeviceId, echoCancellation, noiseSuppression, autoGainControl, audioGain | UserPreferences (Prisma) |
| TTS | preferredTtsProvider, preferredTtsVoice, ttsSpeed, ttsAutoPlay | UserPreferences (Prisma) |
| Voice Agent | preferredVoiceAgent, voiceAgentAutoConnect, voiceAgentMuted | UserPreferences (Prisma) |
| Clinical | defaultDiagnosisView, showMedicalImages, defaultEncounterType | UserPreferences (Prisma) |
| Notifications | emailNotifications, pushNotifications, smsNotifications | UserPreferences (Prisma) |
| Org defaults | defaultLlmSettings, defaultTranscriberSettings, defaultUiSettings | OrganizationSettings.metadata (JSON blobs) |
Tech Stack & Choices
| Concern | Choice | Rationale |
|---|---|---|
| User prefs storage | UserPreferences model in prisma-main | Co-located with User; foreign key guarantees integrity |
| Org defaults storage | OrganizationSettings.defaultLlmSettings / defaultTranscriberSettings / defaultUiSettings (JSON) | Flexible schema for org-specific feature flags; avoids wide column proliferation |
| Resolution engine | UnifiedSettingsService (platform/common/config) | Single source of truth for all three layers; injected into both UserSettingsService and SettingsService |
| LLM config | DDXLLMConfigService (wraps LiteLLM hierarchy) | Resolves available models per user from the LiteLLM routing tier |
| Audit | LoggingService.logAudit() | All settings mutations record oldValue/newValue; GDPR-traceable |
| GDPR delete | UserSettingsService.deletePreferences(userId) | Hard-deletes the UserPreferences row; used by account deletion flow |
Data Flow
Reading unified settings (most common path)
- Frontend calls
GET /api/settings/unifiedwithX-API-Key+X-Gateway-User-ID. SettingsService.getUnifiedSettings(userId, organizationId)callsUnifiedSettingsService.getUserSettings(userId).UnifiedSettingsServicereadsUserPreferencesrow → if field is null, readsOrganizationSettingsJSON blobs → if still null, reads platform env defaults.- Returns a fully populated
UnifiedSettingsDtowith asourceobject:{ llm: "user" | "org" | "system", transcriber: "...", tts: "...", voiceAgent: "...", ui: "..." }. - Frontend uses
sourceto show "using clinic defaults" badges in the Settings UI.
Writing a partial update (e.g. doctor changes LLM model)
- Frontend calls
PATCH /api/settings/llmwith{ reasoningModel: "claude-opus-4-7" }. SettingsService.updateLlmSettings(userId, dto)mapsreasoningModel→litellmReasoningModel+preferredLlmModel(dual-write for legacy compat).prisma.userPreferences.update()writes the row.LoggingService.logAudit()records{ action: "update_llm_settings", oldValue: {...}, newValue: {...} }.- Next call to
GET /settings/unifiedreturnssource.llm = "user".
Resetting to clinic defaults
- Frontend calls
POST /api/settings/reset. SettingsService.resetUserSettings(userId, organizationId)deletes theUserPreferencesrow.- Creates a new row seeded from
OrganizationSettingsdefaults (or platform defaults if org has no overrides).
Implicated Code
ddx-api/src/user-mgmt/user-settings/preferences.service.ts:107—UserSettingsService;getPreferences(userId)at:117; three-layer merge with??coalesce;_sourcesmetadata field returned to callersddx-api/src/user-mgmt/user-settings/preferences.service.ts:192—updatePreferences(),updateUiPreferences(),updateTranscriberPreferences(),updateLlmPreferences()— partial update variants per categoryddx-api/src/organization/settings/settings.service.ts:31—SettingsService;getUnifiedSettings()at:47;sourcefield maps"platform"→"system"for frontend compat at:158–168ddx-api/src/organization/settings/settings.service.ts:288—getOrganizationSettings()/updateOrganizationSettings()— org-level admin endpoints; slug-to-UUID resolution viaOrganizationServiceddx-api/src/organization/settings/settings.service.ts:446—updateLlmSettings(); unified field names (reasoningModel) mapped to legacy Prisma columns (preferredLlmModel,litellmReasoningModel) — dual-write for backward compatddx-api/src/user-mgmt/user-preferences/user-preferences.service.ts—UserPreferencesService; separate module for notification + locale preferencesddx-api/src/platform/common/config/settings.config.ts—UnifiedSettingsService; the three-layer resolution engine used by both services above
Operational Notes
- Layer source transparency: The
sourceobject inGET /settings/unifiedresponse always reflects which layer resolved each category. Frontend should use this to display "Using clinic default" or "Using system default" badges — do not infer from null-checks. - Dual-write LLM fields:
reasoningModel(unified) andpreferredLlmModel+litellmReasoningModel(legacy) are written simultaneously during migration. Both fields must be non-null for backward compat with older TUCAN tools that readpreferredLlmModel. - Org settings JSON blobs:
defaultLlmSettings,defaultTranscriberSettings,defaultUiSettingsonOrganizationSettingsare untyped JSON — changes to the platform default shape may silently shadow org settings. Always test org-level reset after changing platform defaults. - GDPR delete path:
UserSettingsService.deletePreferences(userId)hard-deletes the row (not soft-delete). The deletion is intentional for GDPR Art. 17 right-to-erasure compliance. - Env vars:
LITELLM_BASE_URL,LITELLM_API_KEY,DEEPGRAM_API_KEY,GLADIA_API_KEY,OPENAI_TTS_API_KEY,KOKORO_BASE_URL,LIVEKIT_URL,LIVEKIT_API_KEYcontrol which providers are available; missing keys cause thehas*Keyflags in the settings response to befalse, which disables those provider options in the frontend Settings UI. - Kokoro TTS is deprecated — the
TTSProvider.DUDOXX_KOKOROenum value +KOKORO_BASE_URLenv still exist in the DTO/config, but Kokoro is deprecated (DDX CUDA TTS is canonical, selected viatts.provider; project memoryfeedback_kokoro_tts_deprecated). Do not surface Kokoro as a preferred new default; treat the remaining references as legacy pending garaging.
Related Topics
- User Management —
UserPreferencesrow is created as part of user onboarding; deleted on account removal - Organization Management —
OrganizationSettingsrow is created on tenant provisioning; org admins manage clinic defaults - Audit Logging — all settings mutations are audit-logged with
eventType: "SETTINGS_UPDATE"to theddx_api_logdatabase