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 named apiKey, enabling real-time updates across the app.
  • Writes occur through suspend fun setElevenLabsApiKey(apiKey: String), which validates non-blank input and commits atomically via dataStore.edit.
  • The lifecycle spans provisioning in SettingScreen.kt, persistent storage surviving process death, and reactive consumption by ElevenLabProcessor.kt.
  • Parallel storage for subscriptionStatus follows 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:

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 →