How AccountManager Handles ElevenLabs API Key Storage and Lifecycle in Muse
The AccountManager class persists the ElevenLabs API key using Jetpack DataStore as a reactive Flow<String?>, enabling atomic writes and automatic propagation to consumers like ElevenLabProcessor and the settings UI.
The AccountManager class in the kkoshin/muse repository centralizes ElevenLabs API key management for this Kotlin Multiplatform audio processing app. It orchestrates the complete lifecycle—from user input in the settings screen to reactive consumption by the ElevenLabs HTTP client—using AndroidX DataStore for type-safe, persistent storage.
Persistent Storage with Jetpack DataStore
The manager receives a DataStore<Preferences> instance via constructor injection defined in appModule.kt (available for both iOS and Android targets). It declares a typed preference key using stringPreferencesKey:
private val key = stringPreferencesKey("elevenLabsApiKey")
This key identifies the stored string value within the DataStore's Preferences container, ensuring type safety and avoiding string-typing errors elsewhere in the codebase.
Reactive Read Access via Kotlin Flow
The current API key is exposed as a cold Flow<String?> property named apiKey. The implementation maps the DataStore data snapshot to the stored string value:
val apiKey: Flow<String?> = dataStore.data.map { preferences ->
preferences[key]
}
Any component can collect this flow to receive instantaneous updates when the key changes. This reactive pattern eliminates manual synchronization between the storage layer and consumers such as ElevenLabProcessor.kt or the settings screen.
Writing and Updating the API Key
The setElevenLabsApiKey(apiKey: String) method handles all writes with built-in validation and atomic persistence:
suspend fun setElevenLabsApiKey(apiKey: String) {
check(apiKey.isNotBlank()) { "api key cannot be blank" }
dataStore.edit { preferences ->
preferences[key] = apiKey
}
}
The check assertion ensures the input is non-blank before persisting. Because the write occurs within DataStore.edit, the change is committed to disk transactionally and automatically emitted to all collectors of the apiKey flow.
API Key Lifecycle Management
The AccountManager handles four distinct phases of the API key lifecycle:
Provisioning from the Settings UI
Users enter their ElevenLabs API key in SettingScreen.kt, which invokes accountManager.setElevenLabsApiKey() upon saving. This triggers the validation and persistence logic immediately.
Persistent Storage Across Process Death
The key survives app restarts and process termination because DataStore writes to disk asynchronously. Upon app launch, the apiKey flow emits the previously stored value (or null if unset), allowing ElevenLabProcessor.kt to initialize the HTTP client with existing credentials.
Reactive Observation by Consumers
Components like ElevenLabProcessor collect accountManager.apiKey to configure the ElevenLabs client dynamically. When a user updates the key in settings, the processor receives the new value via the flow and can reinitialize connections without requiring an app restart.
Updates and Subscription Status
Calling setElevenLabsApiKey overwrites any previous value atomically. While no explicit "delete" method exists, the architecture also manages subscriptionStatus through a parallel pattern using a separate statusKey, allowing the app to react to changes in the user's ElevenLabs subscription tier alongside API key changes.
Source Code Integration Points
| File | Role |
|---|---|
muse/src/commonMain/kotlin/io/github/kkoshin/muse/core/manager/AccountManager.kt |
Core storage implementation with apiKey flow and setElevenLabsApiKey method. |
muse/src/iosMain/kotlin/io/github/kkoshin/muse/appModule.ktmuse/src/androidMain/kotlin/io/github/kkoshin/muse/appModule.kt |
Koin DI modules providing the DataStore<Preferences> singleton to AccountManager. |
muse/src/commonMain/kotlin/io/github/kkoshin/muse/feature/setting/SettingScreen.kt |
UI component capturing user input and triggering setElevenLabsApiKey. |
muse/src/commonMain/kotlin/io/github/kkoshin/muse/core/manager/ElevenLabProcessor.kt |
Consumes accountManager.apiKey to configure the ElevenLabs HTTP client reactively. |
muse/src/iosTest/kotlin/io/github/kkoshin/muse/core/manager/AccountManagerTest.kt |
Unit tests verifying key storage, retrieval, and update semantics. |
Summary
- AccountManager uses Jetpack DataStore for type-safe, persistent storage of the ElevenLabs API key in
muse/src/commonMain/kotlin/io/github/kkoshin/muse/core/manager/AccountManager.kt. - The API key is exposed as a reactive
Flow<String?>property namedapiKey, enabling real-time updates across the app. - Writes occur through
suspend fun setElevenLabsApiKey(apiKey: String), which validates non-blank input and commits atomically viadataStore.edit. - The lifecycle spans provisioning in
SettingScreen.kt, persistent storage surviving process death, and reactive consumption byElevenLabProcessor.kt. - Parallel storage for
subscriptionStatusfollows an identical pattern, supporting comprehensive ElevenLabs account management.
Frequently Asked Questions
How is the ElevenLabs API key stored in the Muse app?
The key is stored in AndroidX DataStore as a string preference identified by stringPreferencesKey("elevenLabsApiKey"). The AccountManager class in muse/src/commonMain/kotlin/io/github/kkoshin/muse/core/manager/AccountManager.kt encapsulates all read and write operations, providing a type-safe API over the underlying Preferences storage.
What happens when the API key is updated?
When setElevenLabsApiKey() is called, the method validates that the string is non-blank, then writes it atomically to DataStore via dataStore.edit. This triggers the apiKey flow to emit the new value immediately, causing all collectors—such as the one in ElevenLabProcessor.kt—to receive the update and reconfigure their ElevenLabs client instances without requiring an app restart.
How does ElevenLabProcessor receive API key changes?
ElevenLabProcessor collects the apiKey flow from AccountManager within a coroutine scope. Because AccountManager exposes the key as a cold Flow<String?>, the processor receives emissions whenever the stored value changes, enabling dynamic reconfiguration of the HTTP client credentials as soon as the user updates their settings.
Where is the AccountManager instantiated?
The AccountManager is instantiated through Koin dependency injection defined in appModule.kt for both iOS and Android platforms. The module provides a singleton instance backed by a DataStore<Preferences>, ensuring that all consumers share the same storage state and reactive updates across the entire Kotlin Multiplatform codebase.
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 →