# 04 — Settings & Feature Flags (Part 3) Two layers of configuration — **per-user preferences** and **org-wide system settings / feature flags** — with a clear precedence. Critically: **AI is governed by a feature flag (admin, global), not merely a user preference.** ## User settings (per user) Stored in `user_settings`; editable by the user. | Setting | Values | |---------|--------| | `theme` | system · light · dark (dark-first default) | | `inbox_layout` | density, pane layout, default view/lane, per-account or unified | | `notifications` | channels, quiet hours, priority-only | | `ai_prefs` | per-feature opt-in (summaries, replies, semantic, ask-inbox…) — **only effective if the flag allows** | | `provider_prefs` | default account, sync frequency, signature per account | ## System settings (admin, org-wide) Stored in `system_settings` (singleton); Admin-only ([05](05-admin-system.md)). - `maintenance_mode` (off · read-only · locked-except-admin) · `default_theme` · `registration_open` · org display name · retention defaults. ## Feature flag system `feature_flags` rows: `key · enabled · scope · rollout · description · updated_by/at`. - **`scope = SystemOnly`** — a hard org-wide switch; users cannot override (e.g., `provider.microsoft`, `maintenance.readonly`). - **`scope = UserOverridable`** — a default that a user preference can turn *off* (never *on* beyond what the flag permits) — e.g., `ai.summaries`. - **`rollout`** (jsonb) — optional per-role/percentage gating (e.g., enable a beta for Admins first). ### Evaluation service ```csharp public interface IFeatureFlags { bool IsEnabled(string key, UserContext user); // system flag ∧ scope ∧ role rollout } public interface IAiGate { // the AI-specific resolver bool IsAiEnabled(UserContext user); // ai.enabled (system) ∧ user.ai_prefs.master bool IsAiFeatureEnabled(string feature, UserContext user); // ∧ ai. ∧ user opt-in } ``` - Flags are **cached** with change notification (hot-reload on admin edit); every read is cheap. - All flag reads are **fail-closed**: unknown/errored flag ⇒ treated as **off**. ## AI gating precedence (the key requirement) Effective AI availability is an **AND** down a chain — the system flag is the master gate: ``` AI feature X is available for user U ⇔ feature_flags["ai.enabled"].enabled (admin master switch — SYSTEM) ∧ feature_flags["ai." + X].enabled (per-feature flag — SYSTEM) ∧ providerCapability(X) (Ollama/provider actually available) ∧ user.ai_prefs.master_opt_in (user hasn't disabled AI for themselves) ∧ user.ai_prefs[X] (user opted into this feature) ``` - **Admin turns `ai.enabled` off ⇒ AI vanishes for everyone**, regardless of any user preference. This is the behaviour the brief mandates. - With AI on at the system level, users still choose per-feature. The **Null AI provider + capability flags** (blueprint [06](../06-ai-strategy.md)) mean a disabled path **falls back or hides** — never errors, never blocks core email. ## Settings precedence (general) ``` system default → feature flag (may hard-disable) → user override (only where UserOverridable) ``` ## Maintenance mode - `read-only`: mutations (send/cleanup/label) blocked with a banner; browsing/search stay up. - `locked-except-admin`: only Admins can use the app (for migrations/upgrades). - Enforced at the API via a middleware policy + surfaced as a global banner in the UI. ## Auditing Flag and system-setting changes are **admin actions → audit-logged** (who/what/old→new/when).