# How the Muse Script Editor Manages and Persists Scripts Using the SQLDelight Driver

> Learn how the Muse script editor uses SQLDelight and platform-specific SqlDriver implementations to efficiently manage and persist scripts, ensuring UI responsiveness with background thread operations.

- Repository: [Ko Shin/muse](https://github.com/kkoshin/muse)
- Tags: internals
- Published: 2026-03-05

---

**The Muse app persists scripts through a SQLDelight-generated database accessed via platform-specific `SqlDriver` implementations, with `MuseRepo` providing coroutine-wrapped CRUD operations that execute on background threads to maintain UI responsiveness.**

The **Muse** application (available at `kkoshin/muse`) implements a robust local persistence layer for user-generated scripts using SQLDelight. The architecture separates platform-specific database drivers from a shared Kotlin Multiplatform repository, ensuring consistent script management across Android and iOS while keeping database operations off the main thread.

## SQLDelight Database Architecture

The persistence layer centers on SQLDelight's code generation capabilities. The build configuration in `muse/build.gradle.kts` defines the `AppDatabase` schema, which generates type-safe Kotlin APIs from SQL statements at compile time.

The generated `AppDatabase` class exposes a `scriptQueries` property that serves as the data access object (DAO) for the `script` table. This table stores essential script metadata including UUID-based identifiers, titles, text content, and creation timestamps.

## Platform-Specific Driver Implementation

Database connectivity relies on the `DriverFactory` expect/actual pattern to provide platform-appropriate `SqlDriver` instances.

On Android, the factory creates an `AndroidSqliteDriver`:

```kotlin
// muse/src/androidMain/kotlin/io/github/kkoshin/muse/repo/SqlDriver.android.kt
actual class DriverFactory {
    actual fun createDriver(): SqlDriver =
        AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
}

```

For iOS, the implementation uses `NativeSqliteDriver`:

```kotlin
// muse/src/iosMain/kotlin/io/github/kkoshin/muse/repo/SqlDriver.ios.kt
actual class DriverFactory {
    actual fun createDriver(): SqlDriver =
        NativeSqliteDriver(AppDatabase.Schema, "app.db")
}

```

These factories are injected via Koin in [`appModule.kt`](https://github.com/kkoshin/muse/blob/main/appModule.kt) to instantiate a singleton `AppDatabase` that both platforms share:

```kotlin
val db = AppDatabase(DriverFactory().createDriver())

```

## The MuseRepo Repository Layer

`MuseRepo` (located in [`muse/src/commonMain/kotlin/io/github/kkoshin/muse/repo/MuseRepo.kt`](https://github.com/kkoshin/muse/blob/main/muse/src/commonMain/kotlin/io/github/kkoshin/muse/repo/MuseRepo.kt)) abstracts all database interactions behind suspend functions. The class receives the generated `AppDatabase` and exposes high-level methods for script management.

### Core CRUD Operations

The repository implements four primary operations:

```kotlin
class MuseRepo(database: AppDatabase, private val pathManager: MusePathManager) {
    private val scriptDao = database.scriptQueries

    suspend fun queryAllScripts(): List<Script> = withContext(Dispatchers.IO) {
        scriptDao.queryAllScripts().executeAsList()
            .map { Script(Uuid.parse(it.id), it.title, it.text, it.created_At) }
    }

    suspend fun queryScript(id: Uuid): Script? = withContext(Dispatchers.IO) {
        scriptDao.queryScirptById(id.toString())
            .executeAsOneOrNull()
            ?.let { Script(Uuid.parse(it.id), it.title, it.text, it.created_At) }
    }

    suspend fun insertScript(script: Script) = withContext(Dispatchers.IO) {
        scriptDao.insertScript(
            script.id.toString(),
            script.title,
            script.text,
            script.createAt
        )
    }

    suspend fun deleteScript(id: Uuid) = withContext(Dispatchers.IO) {
        scriptDao.deleteScriptById(id.toString())
    }
}

```

Each method explicitly switches to `Dispatchers.IO` before invoking SQLDelight-generated queries, preventing database operations from blocking the UI thread.

## Script Creation and Persistence Flow

When users create scripts in [`ScriptCreatorScreen.kt`](https://github.com/kkoshin/muse/blob/main/ScriptCreatorScreen.kt), the UI delegates persistence to `MuseRepo`:

```kotlin
// Inside ScriptCreatorScreen.kt – on Save button
scope.launch {
    Script(text = content).let {
        repo.insertScript(it)          // persists via MuseRepo
        onResult(it.id)               // returns the new ID to the caller
    }
}

```

The repository converts the domain `Script` object into primitive SQL parameters (converting UUIDs to strings) and executes the `insertScript` query generated by SQLDelight.

## Loading Scripts for Dashboard and Editor

The dashboard retrieves all persisted scripts through `DashboardViewModel`:

```kotlin
class DashboardViewModel(private val repo: MuseRepo) : ViewModel() {
    private val _scripts = MutableStateFlow<List<Script>>(emptyList())
    val scripts: StateFlow<List<Script>> = _scripts.asStateFlow()

    init {
        viewModelScope.launch {
            _scripts.value = repo.queryAllScripts()
        }
    }
}

```

For the script editor, `EditorViewModel` provides specialized access that splits script text into phrases:

```kotlin
class EditorViewModel(
    private val speechProcessorManager: SpeechProcessorManager,
    private val repo: MuseRepo,
) : ViewModel() {
    suspend fun queryPhrases(scriptId: String): List<String>? =
        repo.queryPhrases(Uuid.parse(scriptId))
}

```

The [`EditorScreen.kt`](https://github.com/kkoshin/muse/blob/main/EditorScreen.kt) consumes this flow to display script content while respecting `MAX_TEXT_LENGTH` constraints (10,000 characters) to avoid performance degradation during phrase extraction.

## Practical Implementation Examples

### Inserting a New Script

```kotlin
suspend fun createSampleScript(repo: MuseRepo) {
    val script = Script(
        id = Uuid.random(),
        title = "My First Script",
        text = "Hello world! This is a test script.",
        createAt = Clock.System.now()
    )
    repo.insertScript(script)               // persisting
    println("Inserted script with id ${script.id}")
}

```

### Retrieving Scripts for a List UI

```kotlin
@Composable
fun ScriptList(repo: MuseRepo) {
    val scope = rememberCoroutineScope()
    var scripts by remember { mutableStateOf<List<Script>>(emptyList()) }

    LaunchedEffect(Unit) {
        scope.launch {
            scripts = repo.queryAllScripts()
        }
    }

    LazyColumn {
        items(scripts) { script ->
            Text(text = script.title)
        }
    }
}

```

### Loading Phrases in the Editor

```kotlin
suspend fun loadPhrases(repo: MuseRepo, scriptId: String): List<String> {
    return repo.queryPhrases(Uuid.parse(scriptId)) ?: emptyList()
}

```

## Summary

- **SQLDelight** generates type-safe `AppDatabase` and `scriptQueries` DAO from SQL schemas defined in `build.gradle.kts`.
- **Platform drivers** (`AndroidSqliteDriver` and `NativeSqliteDriver`) provide native SQLite backends through the `DriverFactory` abstraction.
- **MuseRepo** encapsulates all CRUD logic in suspend functions executing on `Dispatchers.IO`, ensuring thread safety.
- **UI layers** (`ScriptCreatorScreen`, `DashboardViewModel`, `EditorViewModel`) interact with the repository through coroutines, maintaining responsive interfaces during database operations.
- The editor implements text segmentation with length limits to optimize performance during content processing.

## Frequently Asked Questions

### How does Muse handle database threading to prevent UI freezes?

All database operations in `MuseRepo` wrap SQLDelight queries inside `withContext(Dispatchers.IO)` blocks. This ensures that SQLite reads and writes execute on background threads, while the repository exposes suspend functions that allow UI components to await results without blocking the main thread.

### What SQL driver does Muse use on Android versus iOS?

On Android, Muse uses `AndroidSqliteDriver` from the SQLDelight Android driver artifact, configured with the application context. On iOS, it uses `NativeSqliteDriver` from the native driver artifact. Both implement the common `SqlDriver` interface, allowing `MuseRepo` to remain platform-agnostic.

### Where is the database schema defined in the Muse codebase?

The schema is defined through SQLDelight's Gradle plugin configuration in `muse/build.gradle.kts` (lines 154-160), which processes `.sq` files to generate the `AppDatabase` class and query interfaces. The generated code includes the `script` table structure and `ScriptQueries` DAO used by `MuseRepo`.

### How does the editor retrieve script content for display?

`EditorViewModel` calls `repo.queryPhrases(scriptId)`, which fetches the script by UUID and splits the text into manageable phrases. This method caps processing at `MAX_TEXT_LENGTH` (10,000 characters) to prevent ANR states during text segmentation, returning a `List<String>` suitable for the editor UI.