# How FluidVoice Handles Configuration Settings: Architecture and Implementation

> FluidVoice manages settings using a thread-safe SettingsStore singleton. It stores preferences in UserDefaults and sensitive data like API keys in the macOS Keychain.

- Repository: [ALTIC/FluidVoice](https://github.com/altic-dev/FluidVoice)
- Tags: architecture
- Published: 2026-06-30

---

**FluidVoice centralizes all user-configurable state in a thread-safe `SettingsStore` singleton that persists plain-text preferences to `UserDefaults` and sensitive data like API keys to the macOS Keychain.**

The open-source macOS dictation application FluidVoice (altic-dev/FluidVoice) manages user preferences through a robust, observable configuration layer designed for seamless SwiftUI integration. Understanding how FluidVoice handles configuration settings reveals a sophisticated persistence strategy that balances performance, security, and automatic data migration across app versions.

## SettingsStore Singleton Architecture

At the core of FluidVoice's configuration system lies the `SettingsStore` class, defined in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift). This **ObservableObject** singleton provides thread-safe access to all application settings through the static instance `SettingsStore.shared`.

The store initializes by executing migration and normalization helpers during its `init()` method. These helpers—including `migrateProviderAPIKeysIfNeeded()` and `normalizePromptSelectionsIfNeeded()`—ensure legacy data formats upgrade seamlessly and the in-memory model maintains consistency across app updates.

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

```

## Dual Persistence Strategy

FluidVoice employs a dual-layer persistence strategy that separates sensitive credentials from general user preferences.

### UserDefaults for Standard Preferences

Standard configuration values—such as the selected prompt profile, launch-at-startup flags, default prompt overrides, and per-app prompt bindings—persist to `UserDefaults.standard`. The store exposes these as typed properties with custom getters and setters that handle underlying serialization and automatically emit `objectWillChange` notifications for SwiftUI reactivity.

### Keychain for Sensitive Credentials

Provider API keys and other sensitive values never touch `UserDefaults`. Instead, the `KeychainService.shared` wrapper manages secure storage through the macOS Keychain. The store exposes typed helpers like `savedProviders` that internally encrypt and decrypt values using `KeychainService` methods `set(_:forKey:)` and `get(_:forKey:)`.

## Prompt Profile Configuration System

The most elaborate **FluidVoice configuration settings** involve **dictation prompt profiles**—the system prompts that drive the underlying LLM integration.

### Profile Storage and Serialization

Custom prompt profiles store as an array of `DictationPromptProfile` objects encoded with `JSONEncoder` and persisted under the key `Keys.dictationPromptProfiles`. The getter and setter (lines 58-76 in [`SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsStore.swift)) handle JSON serialization transparently, decoding values on access and encoding them on assignment.

### Per-Application Bindings

Users can override default profiles for specific applications using `AppPromptBinding` objects, stored under `Keys.appPromptBindings`. This allows contextual dictation behavior based on the active frontmost application.

### Selection Resolution Logic

The active profile for a given shortcut slot resolves through `dictationPromptSelection(for:)`, which evaluates the "off" flag status, explicitly selected profile ID, built-in "private-AI" shortcuts, or falls back to the default configuration.

## Modular Configuration Extensions

The codebase organizes related configuration logic into focused Swift extensions for maintainability.

### Prompt Routing Scope

`Sources/Fluid/Persistence/SettingsStore+PromptRouting.swift` defines the `PromptRoutingScope` enum distinguishing between `allApps` and `selectedAppsOnly` modes. This extension manages routing scope persistence per dictation mode, allowing users to restrict AI processing to specific applications.

### Launch at Startup Management

`Sources/Fluid/Persistence/SettingsStore+LaunchAtStartup.swift` encapsulates the launch-at-startup functionality through the `launchAtStartupEnabled` property and `refreshLaunchAtStartupStatus()` method. This isolation contains the helper logic required for managing macOS login items and permission handling.

### Command Mode Hotkeys

`Sources/Fluid/Persistence/SettingsStore+CommandMode.swift` persists hotkey configurations for the command-mode UI, keeping keyboard shortcut preferences logically separated from core dictation settings.

## Working with Configuration Values

The `SettingsStore` API simplifies configuration access through property wrappers that automatically synchronize with persistence layers.

### Reading Current Settings

Access configuration values directly through the singleton instance:

```swift
let isEnabled = SettingsStore.shared.enableAIProcessing
let selectedProfileID = SettingsStore.shared.selectedDictationPromptID

```

### Writing and Persisting Changes

Assignment operations automatically persist to `UserDefaults` and trigger UI updates via `objectWillChange`:

```swift
SettingsStore.shared.enableAIProcessing = true
SettingsStore.shared.setSelectedDictationPromptID("my-profile-id", for: .primary)

```

### Managing Custom LLM Providers

When adding providers, API keys route automatically to the Keychain while metadata stores in standard defaults:

```swift
let provider = SavedProvider(
    name: "MyLLM",
    baseURL: "https://api.myllm.com",
    apiKey: "super-secret-key",
    models: ["gpt-4"]
)

var providers = SettingsStore.shared.savedProviders
providers.append(provider)
SettingsStore.shared.savedProviders = providers

```

### Configuring System Integration

Enable launch-at-startup and verify the current state:

```swift
SettingsStore.shared.setLaunchAtStartupEnabled(true)
print("Launch at startup enabled:", SettingsStore.shared.launchAtStartupEnabled)

```

## Configuration Lifecycle and Data Migration

When FluidVoice launches, `SettingsStore.shared` instantiation triggers a sequence of data integrity checks. The initialization process reads all persisted values from both `UserDefaults` and `Keychain`, executes migration helpers for legacy data formats, and publishes the initial state to any observing SwiftUI views.

This architecture ensures that configuration changes reflect immediately in the UI while maintaining durable persistence across app restarts. The migration pattern prevents data loss when updating between versions with different storage schemas, as implemented in the core initialization logic of [`SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/SettingsStore.swift).

## Summary

- **FluidVoice configuration settings** reside in a centralized `SettingsStore` singleton conforming to `ObservableObject` for reactive SwiftUI bindings.
- The system splits persistence between `UserDefaults` (plain-text preferences) and the macOS Keychain (sensitive API keys via `KeychainService`).
- Prompt profiles and per-app bindings use JSON-encoded arrays with dedicated keys like `dictationPromptProfiles` and `appPromptBindings`.
- Configuration logic organizes into focused extensions including `SettingsStore+PromptRouting.swift` and `SettingsStore+LaunchAtStartup.swift`.
- Automatic migration helpers ensure backward compatibility when updating between app versions.

## Frequently Asked Questions

### How does FluidVoice secure API keys for LLM providers?

FluidVoice stores provider API keys exclusively in the macOS Keychain through the `KeychainService` wrapper, never in `UserDefaults`. The `SettingsStore` exposes typed helpers like `savedProviders` that transparently encrypt values using `KeychainService.shared.set(_:forKey:)` and decrypt them via `get(_:forKey:)`, ensuring credentials remain encrypted at rest and inaccessible to other applications.

### Where does FluidVoice store user-created prompt profiles?

User-created dictation prompt profiles persist as JSON-encoded arrays of `DictationPromptProfile` objects under the `Keys.dictationPromptProfiles` key in `UserDefaults`. The `SettingsStore` handles serialization automatically through custom property getters and setters defined in [`Sources/Fluid/Persistence/SettingsStore.swift`](https://github.com/altic-dev/FluidVoice/blob/main/Sources/Fluid/Persistence/SettingsStore.swift), typically around lines 58-76.

### How does FluidVoice handle configuration changes from the user interface?

SwiftUI views bind to `SettingsStore.shared` using `@ObservedObject`. When users modify settings—such as selecting a new prompt profile via `setSelectedDictationPromptID(_:for:)`—the setter immediately updates `UserDefaults` and emits `objectWillChange`, triggering automatic view refreshes without requiring manual UI updates or explicit save actions.

### What happens to existing settings when FluidVoice updates to a new version?

The `SettingsStore` initialization runs migration helpers like `migrateProviderAPIKeysIfNeeded()` and `normalizePromptSelectionsIfNeeded()` to upgrade legacy data formats automatically. This ensures existing user preferences transfer correctly between app versions while maintaining data integrity, consistency, and backward compatibility for older configuration schemas.