How FluidVoice Handles Configuration Settings: Architecture and Implementation

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. 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.

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

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:

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:

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:

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.

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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →