11-registration-and-onboardingwave: W6filled5 citations

User Settings Hierarchy — Unified Preference System

Audiences: developer, internal

User Settings Hierarchy — Unified Preference System

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/unified endpoint returns the fully resolved config for the authenticated user in one call. Layer source is reported in the source object. Partial PATCH updates available per category: llm, transcriber, tts, voiceAgent, ui.
  • Internal (ops/support): Org admins update OrganizationSettings via PATCH /settings/organizations/:orgId. Super admins control platform defaults via env vars. All settings mutations are audit-logged via LoggingService.logAudit().

Architecture

The settings system spans three NestJS modules that collaborate through UnifiedSettingsService:

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

CategoryFieldsStorage
UItheme, locale, fontSize, highContrast, reduceMotion, dateFormat, timeFormat, timezone, sidebarCollapsed, compactMode, defaultPageSizeUserPreferences (Prisma)
LLMlitellmReasoningModel, llmProvider, llmTemperature, llmMaxTokens, litellmBudget, litellmRpmLimit, litellmTpmLimit, aiSuggestionsEnabled, aiAutoSummarizeUserPreferences (Prisma)
TranscriberdefaultTranscriber, transcriberLanguage, transcriberModel, diarizeEnabled, punctuateEnabled, smartFormatEnabled, audioDeviceId, echoCancellation, noiseSuppression, autoGainControl, audioGainUserPreferences (Prisma)
TTSpreferredTtsProvider, preferredTtsVoice, ttsSpeed, ttsAutoPlayUserPreferences (Prisma)
Voice AgentpreferredVoiceAgent, voiceAgentAutoConnect, voiceAgentMutedUserPreferences (Prisma)
ClinicaldefaultDiagnosisView, showMedicalImages, defaultEncounterTypeUserPreferences (Prisma)
NotificationsemailNotifications, pushNotifications, smsNotificationsUserPreferences (Prisma)
Org defaultsdefaultLlmSettings, defaultTranscriberSettings, defaultUiSettingsOrganizationSettings.metadata (JSON blobs)

Tech Stack & Choices

ConcernChoiceRationale
User prefs storageUserPreferences model in prisma-mainCo-located with User; foreign key guarantees integrity
Org defaults storageOrganizationSettings.defaultLlmSettings / defaultTranscriberSettings / defaultUiSettings (JSON)Flexible schema for org-specific feature flags; avoids wide column proliferation
Resolution engineUnifiedSettingsService (platform/common/config)Single source of truth for all three layers; injected into both UserSettingsService and SettingsService
LLM configDDXLLMConfigService (wraps LiteLLM hierarchy)Resolves available models per user from the LiteLLM routing tier
AuditLoggingService.logAudit()All settings mutations record oldValue/newValue; GDPR-traceable
GDPR deleteUserSettingsService.deletePreferences(userId)Hard-deletes the UserPreferences row; used by account deletion flow

Data Flow

Reading unified settings (most common path)

  1. Frontend calls GET /api/settings/unified with X-API-Key + X-Gateway-User-ID.
  2. SettingsService.getUnifiedSettings(userId, organizationId) calls UnifiedSettingsService.getUserSettings(userId).
  3. UnifiedSettingsService reads UserPreferences row → if field is null, reads OrganizationSettings JSON blobs → if still null, reads platform env defaults.
  4. Returns a fully populated UnifiedSettingsDto with a source object: { llm: "user" | "org" | "system", transcriber: "...", tts: "...", voiceAgent: "...", ui: "..." }.
  5. Frontend uses source to show "using clinic defaults" badges in the Settings UI.

Writing a partial update (e.g. doctor changes LLM model)

  1. Frontend calls PATCH /api/settings/llm with { reasoningModel: "claude-opus-4-7" }.
  2. SettingsService.updateLlmSettings(userId, dto) maps reasoningModellitellmReasoningModel + preferredLlmModel (dual-write for legacy compat).
  3. prisma.userPreferences.update() writes the row.
  4. LoggingService.logAudit() records { action: "update_llm_settings", oldValue: {...}, newValue: {...} }.
  5. Next call to GET /settings/unified returns source.llm = "user".

Resetting to clinic defaults

  1. Frontend calls POST /api/settings/reset.
  2. SettingsService.resetUserSettings(userId, organizationId) deletes the UserPreferences row.
  3. Creates a new row seeded from OrganizationSettings defaults (or platform defaults if org has no overrides).

Implicated Code

  • ddx-api/src/user-mgmt/user-settings/preferences.service.ts:107UserSettingsService; getPreferences(userId) at :117; three-layer merge with ?? coalesce; _sources metadata field returned to callers
  • ddx-api/src/user-mgmt/user-settings/preferences.service.ts:192updatePreferences(), updateUiPreferences(), updateTranscriberPreferences(), updateLlmPreferences() — partial update variants per category
  • ddx-api/src/organization/settings/settings.service.ts:31SettingsService; getUnifiedSettings() at :47; source field maps "platform""system" for frontend compat at :158–168
  • ddx-api/src/organization/settings/settings.service.ts:288getOrganizationSettings() / updateOrganizationSettings() — org-level admin endpoints; slug-to-UUID resolution via OrganizationService
  • ddx-api/src/organization/settings/settings.service.ts:446updateLlmSettings(); unified field names (reasoningModel) mapped to legacy Prisma columns (preferredLlmModel, litellmReasoningModel) — dual-write for backward compat
  • ddx-api/src/user-mgmt/user-preferences/user-preferences.service.tsUserPreferencesService; separate module for notification + locale preferences
  • ddx-api/src/platform/common/config/settings.config.tsUnifiedSettingsService; the three-layer resolution engine used by both services above

Operational Notes

  • Layer source transparency: The source object in GET /settings/unified response 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) and preferredLlmModel + litellmReasoningModel (legacy) are written simultaneously during migration. Both fields must be non-null for backward compat with older TUCAN tools that read preferredLlmModel.
  • Org settings JSON blobs: defaultLlmSettings, defaultTranscriberSettings, defaultUiSettings on OrganizationSettings are 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_KEY control which providers are available; missing keys cause the has*Key flags in the settings response to be false, which disables those provider options in the frontend Settings UI.
  • Kokoro TTS is deprecated — the TTSProvider.DUDOXX_KOKORO enum value + KOKORO_BASE_URL env still exist in the DTO/config, but Kokoro is deprecated (DDX CUDA TTS is canonical, selected via tts.provider; project memory feedback_kokoro_tts_deprecated). Do not surface Kokoro as a preferred new default; treat the remaining references as legacy pending garaging.
  • User ManagementUserPreferences row is created as part of user onboarding; deleted on account removal
  • Organization ManagementOrganizationSettings row is created on tenant provisioning; org admins manage clinic defaults
  • Audit Logging — all settings mutations are audit-logged with eventType: "SETTINGS_UPDATE" to the ddx_api_log database
    User Settings Hierarchy — Unified Preference System — Dudoxx Docs | Dudoxx