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
SettingsStoresingleton conforming toObservableObjectfor reactive SwiftUI bindings. - The system splits persistence between
UserDefaults(plain-text preferences) and the macOS Keychain (sensitive API keys viaKeychainService). - Prompt profiles and per-app bindings use JSON-encoded arrays with dedicated keys like
dictationPromptProfilesandappPromptBindings. - Configuration logic organizes into focused extensions including
SettingsStore+PromptRouting.swiftandSettingsStore+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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →