# How FluidVoice SettingsStore Works: Complete Technical Guide

> Explore the technical guide to FluidVoice's SettingsStore. Learn how this observable singleton manages app state with UserDefaults, Keychain, and SwiftUI for reactive updates.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: technical-guide
- Published: 2026-07-08

---

**SettingsStore is the central, observable singleton that persists all user-configurable state in FluidVoice using UserDefaults, Keychain, and SwiftUI's ObservableObject protocol to provide reactive updates across the macOS app.**

FluidVoice is an open-source macOS dictation application developed by altic-dev. At its core, the `SettingsStore` class manages everything from AI prompt configurations to launch-at-login preferences. This singleton observable object lives in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift) and serves as the single source of truth for all user settings, extended across modular files to maintain clean separation of concerns.

## Singleton Architecture and Observable Pattern

The SettingsStore implements a strict singleton pattern to ensure consistency across the application lifecycle.

### The Shared Instance

The store exposes a single shared instance via `static let shared`, preventing multiple initialization points that could cause state desynchronization. The class declaration in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift) conforms to `ObservableObject`, enabling SwiftUI views to react instantly to configuration changes:

```swift
final class SettingsStore: ObservableObject {
    static let shared = SettingsStore()
    private let defaults = UserDefaults.standard
    private let keychain = KeychainService.shared
    // ...
}

```

### Initialization and Migration

The private initializer runs a cascade of migration helpers to maintain backward compatibility when users upgrade FluidVoice. According to the source code, this includes `migrateTranscriptionStartSoundIfNeeded()` and `migrateProviderAPIKeysIfNeeded()`, ensuring legacy data formats transition smoothly to current schemas.

The initialization also calls `refreshLaunchAtStartupStatus(clearError: true, logMismatch: false)` to synchronize the UI state with the actual macOS login item status at launch.

## Data Persistence Strategy

### UserDefaults Integration

All user-visible preferences read from and write to `UserDefaults.standard`. Each property uses well-named keys defined in enum extensions. For example, the launch-at-startup preference uses keys defined in `Sources/Fluid/Persistence/SettingsStore+LaunchAtStartup.swift`:

```swift
private enum LaunchAtStartupKeys {
    static let preference = "LaunchAtStartup"
    static let legacyCompatibilityItem = "LaunchAtStartupCompatibilityFallback"
}

```

### Keychain for Sensitive Data

The store maintains a reference to `KeychainService.shared` for secure storage of API keys and other sensitive credentials, keeping them out of unsecured UserDefaults.

## Prompt Profile Management System

FluidVoice supports named **dictation prompt profiles** that allow users to customize AI behavior for different contexts.

### DictationPromptProfile Structure

Profiles are stored as an array of JSON-encoded objects representing the `DictationPromptProfile` struct. Each profile contains a name, prompt text, mode (dictate or edit), and context inclusion flag. The getter and setter in [`SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsStore.swift) handle the encoding/decoding automatically:

```swift
var dictationPromptProfiles: [DictationPromptProfile] {
    get { /* decode from defaults */ }
    set { /* encode & store */ }
}

```

### Selection Enumeration

The `DictationPromptSelection` enum defines four states:
- **.off** – feature disabled
- **.default** – use built-in default
- **.privateAI** – use internal Private-AI prompt
- **.profile(id)** – use a user-created profile

The currently selected option persists under the key `selectedDictationPromptID`.

### Prompt Routing Scope

As implemented in `Sources/Fluid/Persistence/SettingsStore+PromptRouting.swift`, the `PromptRoutingScope` determines whether the selected prompt applies globally or only to specific applications. This scope is stored per mode (dictate vs. edit) and evaluated during prompt resolution.

## App-Specific Prompt Resolution

### The Binding System

For per-application overrides, SettingsStore maintains an array of `AppPromptBinding` objects. Each binding records the target `PromptMode`, the app's bundle identifier, and an optional forced `promptID`. The `upsertAppPromptBinding(for:appBundleID:appName:promptID:)` method creates or updates these bindings:

```swift
func upsertAppPromptBinding(
    for mode: PromptMode,
    appBundleID: String,
    appName: String,
    promptID: String?
) { /* ... */ }

```

### Resolution Hierarchy

When resolving the effective prompt for a specific mode and app, the store evaluates this priority chain:

1. **App binding** (if present, uses specified profile or default)
2. **Prompt-routing scope** (if "selected apps only" and app not in list, falls back to built-in)
3. **User-selected profile** (from global selection)
4. **Built-in default** (or user-provided override)

The core resolver `promptResolution(for:appBundleID:)` implements this logic in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift) (lines 443-511).

### Default Prompt Overrides

Users may replace built-in defaults via `defaultDictationPromptOverride` and `defaultEditPromptOverride` keys. The store automatically strips any hidden base prompt (immutable instruction parts) before persisting overrides, ensuring the UI displays only the editable body.

## macOS System Integration

### Launch at Startup

The `Sources/Fluid/Persistence/SettingsStore+LaunchAtStartup.swift` extension bridges FluidVoice settings with macOS login items. The `setLaunchAtStartup(_:)` method registers or unregisters the service using `SMAppService` and updates the stored preference:

```swift
func setLaunchAtStartup(_ enabled: Bool) {
    // Registers/unregisters SMAppService and syncs state
}

```

The `refreshLaunchAtStartupStatus` method reads the real login-item state via `SMAppService.mainApp.status` and forces the stored preference to match the system reality, preventing UI drift when users modify login items outside the app.

### Error Handling

The store exposes `launchAtStartupErrorMessage` and `launchAtStartupStatusMessage` properties to communicate macOS permission issues (such as unsigned builds) to the UI in human-readable terms.

## Nemotron Language Configuration

As defined in `Sources/Fluid/Persistence/SettingsStore+NemotronLanguage.swift`, the `NemotronLanguage` struct enumerates supported language identifiers for the Nemotron LLM integration. It provides `displayName` properties for UI presentation and handles legacy identifier mapping to ensure settings persist across model updates.

## Practical Code Examples

### Toggling Launch at Startup

```swift
import Fluid

let store = SettingsStore.shared
store.setLaunchAtStartup(true)          // Enable
print(store.launchAtStartupEnabled)     // → true (if macOS allowed it)
print(store.launchAtStartupStatusMessage)

```

### Adding a Custom Dictation Profile

```swift
import Fluid

let store = SettingsStore.shared

let newProfile = SettingsStore.DictationPromptProfile(
    name: "My Academic Prompt",
    prompt: SettingsStore.baseDictationPromptText() + "\n\nPlease keep citations accurate.",
    mode: .dictate,
    includeContext: true
)

var profiles = store.dictationPromptProfiles
profiles.append(newProfile)
store.dictationPromptProfiles = profiles   // Persists via UserDefaults

```

### Resolving the Effective System Prompt

```swift
let mode = SettingsStore.PromptMode.dictate
let systemPrompt = SettingsStore.shared.effectiveSystemPrompt(for: mode)
let userMessage = SettingsStore.renderDictationUserMessage(
    promptText: systemPrompt,
    transcript: "send an email tomorrow"
)
// userMessage now contains the full prompt + transcript ready for the LLM

```

## Summary

- **SettingsStore** is a singleton `ObservableObject` located in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift) that serves as the single source of truth for FluidVoice configuration.
- **Persistence** uses `UserDefaults` for preferences and `KeychainService` for sensitive data, with automatic JSON encoding for complex types like prompt profiles.
- **Prompt resolution** follows a hierarchical chain: app-specific bindings → routing scope → user selection → default overrides.
- **macOS integration** synchronizes launch-at-login status with `SMAppService` and handles migration automatically during initialization.
- **Reactive updates** via `objectWillChange.send()` ensure SwiftUI views reflect changes immediately without manual notification management.

## Frequently Asked Questions

### Where is the SettingsStore located in the FluidVoice repository?

The core implementation resides in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift). The class is extended across several modular files including `SettingsStore+PromptRouting.swift`, `SettingsStore+LaunchAtStartup.swift`, and `SettingsStore+NemotronLanguage.swift` to organize functionality by domain while maintaining the singleton pattern.

### How does SettingsStore handle data migration between app versions?

During initialization, the private `init()` method executes migration helpers such as `migrateTranscriptionStartSoundIfNeeded()` and `migrateProviderAPIKeysIfNeeded()`. These methods check for legacy data formats and transform them to current schemas before the store becomes available to the rest of the application, ensuring seamless upgrades for existing users.

### What is the difference between prompt profiles and prompt selections?

**Prompt profiles** are reusable templates (structs containing name, text, and settings) stored as JSON arrays. **Prompt selections** represent the current operational state—whether the feature is off, using the default, using the private AI option, or referencing a specific profile by ID. The selection determines which profile (if any) is active, while the profile defines the actual AI behavior.

### How does SettingsStore sync with macOS login items?

The store calls `refreshLaunchAtStartupStatus(clearError: true, logMismatch: false)` during initialization to read the actual system state via `SMAppService.mainApp.status`. When users toggle the preference via `setLaunchAtStartup(_:)`, the method registers or unregisters the login item and immediately syncs the stored boolean to match the result, handling errors like unsigned build restrictions by exposing them through `launchAtStartupErrorMessage`.