Repository: thunderbird/thunderbird-android (language: kotlin)

Issue:
Add DataStore to persist key-value based preferences
Introduce a new `core:datastore` module to provide a robust, scalable, and flexible data storage solution for core features. 

**Acceptance Criteria:**

- Module supports versioning and migration
- API is documented and extensible
- Tests cover migration and versioning scenarios
- Integrates with existing core modules


Context: #9370 


Discussion hints:
nan

Pull request: feat(core): add configstore as key-value store
Resolves #9704 

Test command: ./gradlew :core:configstore:impl-backend:testDebugUnitTest --tests "net.thunderbird.core.configstore.backend.DefaultDataStoreConfigBackendTest" --tests "net.thunderbird.core.configstore.backend.DataStoreConfigBackendFactoryTest" :core:configstore:api:testDebugUnitTest --tests "net.thunderbird.core.configstore.TestConfigDefinition" --tests "net.thunderbird.core.configstore.TestConfigStore" --tests "net.thunderbird.core.configstore.TestConfigMapper"

Fix patch:
diff --git a/app-common/build.gradle.kts b/app-common/build.gradle.kts
index 56ef6856522..871afb85bca 100644
--- a/app-common/build.gradle.kts
+++ b/app-common/build.gradle.kts
@@ -27,6 +27,9 @@ dependencies {
     implementation(projects.core.logging.implLegacy)
     implementation(projects.core.logging.implFile)
 
+    implementation(projects.core.configstore.api)
+    implementation(projects.core.configstore.implBackend)
+
     implementation(projects.core.featureflag)
     implementation(projects.core.ui.legacy.theme2.common)
 
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
index c9d1d85c7d9..a1d7430af48 100644
--- a/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/AppCommonCoreModule.kt
@@ -1,70 +1,13 @@
 package net.thunderbird.app.common.core
 
-import android.content.Context
-import kotlin.time.ExperimentalTime
-import net.thunderbird.app.common.core.logging.DefaultLogLevelManager
-import net.thunderbird.core.common.inject.getList
-import net.thunderbird.core.common.inject.singleListOf
-import net.thunderbird.core.logging.DefaultLogger
-import net.thunderbird.core.logging.LogLevel
-import net.thunderbird.core.logging.LogLevelManager
-import net.thunderbird.core.logging.LogLevelProvider
-import net.thunderbird.core.logging.LogSink
-import net.thunderbird.core.logging.Logger
-import net.thunderbird.core.logging.composite.CompositeLogSink
-import net.thunderbird.core.logging.console.ConsoleLogSink
-import net.thunderbird.core.logging.file.AndroidFileSystemManager
-import net.thunderbird.core.logging.file.FileLogSink
+import net.thunderbird.app.common.core.configstore.appCommonCoreConfigStoreModule
+import net.thunderbird.app.common.core.logging.appCommonCoreLogger
 import org.koin.core.module.Module
-import org.koin.core.qualifier.named
-import org.koin.dsl.bind
 import org.koin.dsl.module
 
 val appCommonCoreModule: Module = module {
-    single<LogLevelManager> {
-        DefaultLogLevelManager()
-    }.bind<LogLevelProvider>()
-
-    singleListOf<LogSink>(
-        { ConsoleLogSink(level = LogLevel.VERBOSE) },
+    includes(
+        appCommonCoreConfigStoreModule,
+        appCommonCoreLogger,
     )
-
-    single<CompositeLogSink> {
-        CompositeLogSink(
-            logLevelProvider = get(),
-            sinks = getList(),
-        )
-    }
-
-    single<Logger> {
-        @OptIn(ExperimentalTime::class)
-        DefaultLogger(
-            sink = get<CompositeLogSink>(),
-        )
-    }
-
-    single<CompositeLogSink>(named(SYNC_DEBUG_LOG)) {
-        CompositeLogSink(
-            logLevelProvider = get(),
-            sinks = getList(),
-        )
-    }
-
-    single<FileLogSink>(named(SYNC_DEBUG_LOG)) {
-        FileLogSink(
-            level = LogLevel.DEBUG,
-            fileName = "thunderbird-sync-debug",
-            fileLocation = get<Context>().filesDir.path,
-            fileSystemManager = AndroidFileSystemManager(get<Context>().contentResolver),
-        )
-    }
-
-    single<Logger>(named(SYNC_DEBUG_LOG)) {
-        @OptIn(ExperimentalTime::class)
-        DefaultLogger(
-            sink = get<CompositeLogSink>(named(SYNC_DEBUG_LOG)),
-        )
-    }
 }
-
-internal const val SYNC_DEBUG_LOG = "syncDebug"
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/configstore/ConfigStoreModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/configstore/ConfigStoreModule.kt
new file mode 100644
index 00000000000..7a0034382e4
--- /dev/null
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/configstore/ConfigStoreModule.kt
@@ -0,0 +1,30 @@
+package net.thunderbird.app.common.core.configstore
+
+import net.thunderbird.core.configstore.backend.AndroidConfigBackendFileManager
+import net.thunderbird.core.configstore.backend.ConfigBackendFactory
+import net.thunderbird.core.configstore.backend.ConfigBackendFileManager
+import net.thunderbird.core.configstore.backend.ConfigBackendProvider
+import net.thunderbird.core.configstore.backend.DataStoreConfigBackendFactory
+import net.thunderbird.core.configstore.backend.DefaultConfigBackendProvider
+import org.koin.dsl.module
+
+val appCommonCoreConfigStoreModule = module {
+
+    single<ConfigBackendFileManager> {
+        AndroidConfigBackendFileManager(
+            context = get(),
+        )
+    }
+
+    single<ConfigBackendFactory> {
+        DataStoreConfigBackendFactory(
+            fileManager = get(),
+        )
+    }
+
+    single<ConfigBackendProvider> {
+        DefaultConfigBackendProvider(
+            backendFactory = get(),
+        )
+    }
+}
diff --git a/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt
new file mode 100644
index 00000000000..e31b9f2c0b9
--- /dev/null
+++ b/app-common/src/main/kotlin/net/thunderbird/app/common/core/logging/LoggerModule.kt
@@ -0,0 +1,73 @@
+package net.thunderbird.app.common.core.logging
+
+import android.content.Context
+import kotlin.time.ExperimentalTime
+import net.thunderbird.app.common.BuildConfig
+import net.thunderbird.core.common.inject.getList
+import net.thunderbird.core.common.inject.singleListOf
+import net.thunderbird.core.logging.DefaultLogger
+import net.thunderbird.core.logging.LogLevel
+import net.thunderbird.core.logging.LogLevelManager
+import net.thunderbird.core.logging.LogLevelProvider
+import net.thunderbird.core.logging.LogSink
+import net.thunderbird.core.logging.Logger
+import net.thunderbird.core.logging.composite.CompositeLogSink
+import net.thunderbird.core.logging.console.ConsoleLogSink
+import net.thunderbird.core.logging.file.AndroidFileSystemManager
+import net.thunderbird.core.logging.file.FileLogSink
+import org.koin.core.qualifier.named
+import org.koin.dsl.bind
+import org.koin.dsl.module
+
+val appCommonCoreLogger = module {
+    single<LogLevel> {
+        if (BuildConfig.DEBUG) LogLevel.VERBOSE else LogLevel.INFO
+    }
+
+    single<LogLevelManager> {
+        DefaultLogLevelManager()
+    }.bind<LogLevelProvider>()
+
+    singleListOf<LogSink>(
+        { ConsoleLogSink(level = LogLevel.VERBOSE) },
+    )
+
+    single<CompositeLogSink> {
+        CompositeLogSink(
+            logLevelProvider = get(),
+            sinks = getList(),
+        )
+    }
+
+    single<Logger> {
+        @OptIn(ExperimentalTime::class)
+        DefaultLogger(
+            sink = get<CompositeLogSink>(),
+        )
+    }
+
+    single<CompositeLogSink>(named(SYNC_DEBUG_LOG)) {
+        CompositeLogSink(
+            logLevelProvider = get(),
+            sinks = getList(),
+        )
+    }
+
+    single<FileLogSink>(named(SYNC_DEBUG_LOG)) {
+        FileLogSink(
+            level = LogLevel.DEBUG,
+            fileName = "thunderbird-sync-debug",
+            fileLocation = get<Context>().filesDir.path,
+            fileSystemManager = AndroidFileSystemManager(get<Context>().contentResolver),
+        )
+    }
+
+    single<Logger>(named(SYNC_DEBUG_LOG)) {
+        @OptIn(ExperimentalTime::class)
+        DefaultLogger(
+            sink = get<CompositeLogSink>(named(SYNC_DEBUG_LOG)),
+        )
+    }
+}
+
+internal const val SYNC_DEBUG_LOG = "syncDebug"
diff --git a/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt b/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt
index 4297db77d18..a4659e2ea8c 100644
--- a/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt
+++ b/app-k9mail/dependencies/fossReleaseRuntimeClasspath.txt
@@ -69,10 +69,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt b/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt
index 0b267e7f3cb..603d646854a 100644
--- a/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt
+++ b/app-k9mail/dependencies/fullReleaseRuntimeClasspath.txt
@@ -69,10 +69,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt b/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt
index c9e218ad5b8..5e699131648 100644
--- a/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt
+++ b/app-thunderbird/dependencies/fossBetaRuntimeClasspath.txt
@@ -74,10 +74,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt b/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt
index c9e218ad5b8..5e699131648 100644
--- a/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt
+++ b/app-thunderbird/dependencies/fossDailyRuntimeClasspath.txt
@@ -74,10 +74,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt b/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt
index c9e218ad5b8..5e699131648 100644
--- a/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt
+++ b/app-thunderbird/dependencies/fossReleaseRuntimeClasspath.txt
@@ -74,10 +74,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt b/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt
index 760ee3f2124..8fbe850f05e 100644
--- a/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt
+++ b/app-thunderbird/dependencies/fullBetaRuntimeClasspath.txt
@@ -74,10 +74,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt b/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt
index 760ee3f2124..8fbe850f05e 100644
--- a/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt
+++ b/app-thunderbird/dependencies/fullDailyRuntimeClasspath.txt
@@ -74,10 +74,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt b/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt
index 760ee3f2124..8fbe850f05e 100644
--- a/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt
+++ b/app-thunderbird/dependencies/fullReleaseRuntimeClasspath.txt
@@ -74,10 +74,18 @@ androidx.core:core:1.16.0
 androidx.cursoradapter:cursoradapter:1.0.0
 androidx.customview:customview-poolingcontainer:1.0.0
 androidx.customview:customview:1.1.0
-androidx.datastore:datastore-core:1.0.0
-androidx.datastore:datastore-preferences-core:1.0.0
-androidx.datastore:datastore-preferences:1.0.0
-androidx.datastore:datastore:1.0.0
+androidx.datastore:datastore-android:1.1.7
+androidx.datastore:datastore-core-android:1.1.7
+androidx.datastore:datastore-core-okio-jvm:1.1.7
+androidx.datastore:datastore-core-okio:1.1.7
+androidx.datastore:datastore-core:1.1.7
+androidx.datastore:datastore-preferences-android:1.1.7
+androidx.datastore:datastore-preferences-core-android:1.1.7
+androidx.datastore:datastore-preferences-core:1.1.7
+androidx.datastore:datastore-preferences-external-protobuf:1.1.7
+androidx.datastore:datastore-preferences-proto:1.1.7
+androidx.datastore:datastore-preferences:1.1.7
+androidx.datastore:datastore:1.1.7
 androidx.documentfile:documentfile:1.0.0
 androidx.drawerlayout:drawerlayout:1.1.1
 androidx.dynamicanimation:dynamicanimation:1.0.0
diff --git a/core/configstore/README.md b/core/configstore/README.md
new file mode 100644
index 00000000000..2f1b263e0be
--- /dev/null
+++ b/core/configstore/README.md
@@ -0,0 +1,193 @@
+# Core - ConfigStore
+
+A small, type-safe configuration store built on AndroidX DataStore Preferences. Use it to define a schema for your 
+feature settings, observe them as a Flow, update atomically, and migrate between versions.
+
+## How it works at a glance
+
+```mermaid
+graph TB
+    subgraph FEATURE[App Feature]
+        FEATURE_DATASTORE[FeatureDataStore]
+    end
+  
+    subgraph CORE_API[ConfigStore API]
+        CONFIG_STORE["`ConfigStore<T>`"]
+        BASE_CONFIG_STORE[BaseConfigStore]
+        CONFIG_DEFINITION["`ConfigDefinition<T>`"]
+    end
+  
+    subgraph CORE_BACKEND[ConfigStore Backend]
+        BACKEND_PROVIDER[ConfigBackendProvider]
+        DATASTORE_BACKEND[DataStoreConfigBackend / DefaultDataStoreConfigBackend]
+    end
+  
+    subgraph STORAGE[Storage]
+        PREFERENCES_FILE["DataStore Preferences file &lt;backend&gt;.preferences_pb"]
+    end
+
+    FEATURE_DATASTORE --> |uses| CONFIG_STORE
+    CONFIG_STORE --> |delegates to| BASE_CONFIG_STORE
+    BASE_CONFIG_STORE --> |reads schema| CONFIG_DEFINITION
+    BASE_CONFIG_STORE --> |requests| BACKEND_PROVIDER
+    BACKEND_PROVIDER --> |creates| DATASTORE_BACKEND
+    DATASTORE_BACKEND --> |persists to| STORAGE
+    BASE_CONFIG_STORE -..-> |on start| MIGRATION_CHECK{Migration?}
+    MIGRATION_CHECK --> |version < definition.version| RUN_MIGRATION[Run migration - write removals]
+    
+    classDef feature fill:#d9ffd9,stroke:#000000,color:#000000
+    classDef feature_module fill:#33cc33,stroke:#000000,color:#000000
+    classDef core fill:#e6cce6,stroke:#000000,color:#000000
+    classDef core_module fill:#cc99cc,stroke:#000000,color:#000000
+    classDef library fill:#fff0d0,stroke:#000000,color:#000000
+    classDef library_module fill:#ffaa33,stroke:#000000,color:#000000
+    
+    linkStyle default stroke:#999,stroke-width:2px
+    linkStyle 6 stroke:#999,stroke-width:2px
+
+    class FEATURE feature
+    class FEATURE_DATASTORE feature_module
+    class CORE_API,CORE_BACKEND core
+    class CONFIG_STORE,BASE_CONFIG_STORE,CONFIG_DEFINITION,BACKEND_PROVIDER,DATASTORE_BACKEND core_module
+    class STORAGE library
+    class PREFERENCES_FILE library_module
+```
+
+### Key concepts
+
+- Each feature declares a `ConfigDefinition` with:
+  - a version number (for migrations)
+  - an id (backend name and feature name)
+  - keys (typed key definitions)
+  - a mapper to convert between `Config` (key-value pairs) and your typed model
+  - migration logic if needed
+- `BaseConfigStore` runs migrations once per version and delegates persistence to a backend (default: DataStore Preferences).
+- Files are grouped per backend: **one file per backend ID:** `<backend>.preferences_pb`.
+
+## Quick start
+
+### 1. Define your configuration
+
+```kotlin
+// Keys
+object MyFeatureKeys {
+    val ENABLED = ConfigKey.BooleanKey("myfeature_enabled")
+    val USERNAME = ConfigKey.StringKey("myfeature_username")
+}
+
+// Model
+data class MyFeatureConfig(
+    val enabled: Boolean,
+    val username: String,
+)
+
+// Mapper
+object MyFeatureMapper : ConfigMapper<MyFeatureConfig> {
+    override fun toConfig(obj: MyFeatureConfig): Config = Config().apply {
+        this[MyFeatureKeys.ENABLED] = obj.enabled
+        this[MyFeatureKeys.USERNAME] = obj.username
+    }
+
+    override fun fromConfig(config: Config): MyFeatureConfig? {
+        val enabled = config[MyFeatureKeys.ENABLED] ?: return null
+        val username = config[MyFeatureKeys.USERNAME] ?: return null
+        return MyFeatureConfig(enabled, username)
+    }
+}
+
+// Migration (no-op example)
+object MyFeatureMigration : ConfigMigration {
+    override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult =
+        ConfigMigrationResult.NoOp
+}
+
+// Definition
+object MyFeatureDefinition : ConfigDefinition<MyFeatureConfig> {
+    override val version = 1
+    override val id = ConfigId(backend = "myfeature", feature = "settings")
+    override val mapper = MyFeatureMapper
+    override val defaultValue = MyFeatureConfig(false, "")
+    override val keys = listOf(MyFeatureKeys.ENABLED, MyFeatureKeys.USERNAME)
+    override val migration: ConfigMigration = MyFeatureMigration
+}
+```
+
+### 2. Create a store
+
+```kotlin
+class MyFeatureConfigStore(
+    provider: ConfigBackendProvider,
+) : BaseConfigStore<MyFeatureConfig>(
+    provider = provider,
+    definition = MyFeatureDefinition,
+)
+```
+
+### 3. Observe and update
+
+```kotlin
+// Observe
+val job = coroutineScope.launch {
+    myFeatureConfigStore.config.collect { cfg ->
+        // react to changes
+    }
+}
+
+// Update atomically
+coroutineScope.launch {
+    myFeatureConfigStore.update { current ->
+        val cfg = current ?: MyFeatureDefinition.defaultValue
+        cfg.copy(enabled = true)
+    }
+}
+
+// Clear all stored values for this backend file
+coroutineScope.launch { myFeatureConfigStore.clear() }
+```
+
+## Wiring
+
+- **Android**: use `AndroidConfigBackendFileManager` to store under `Context.filesDir`.
+- **JVM**: use `JvmConfigBackendFileManager` with a working directory.
+
+Example with **Koin**:
+
+```kotlin
+val appCommonCoreConfigStoreModule = module {
+    single<ConfigBackendFileManager> { AndroidConfigBackendFileManager(get()) }
+    single<ConfigBackendFactory> { DataStoreConfigBackendFactory(fileManager = get()) }
+    single<ConfigBackendProvider> { DefaultConfigBackendProvider(backendFactory = get()) }
+}
+```
+
+## Custom backends
+
+While the default implementation uses AndroidX DataStore Preferences, the API supports other backends.
+
+- Implement ConfigBackend to provide:
+  - read(keys): Flow<Config>
+  - update(keys, transform)
+  - clear(), readVersion(), writeVersion(), removeKeys()
+- Provide your backend via ConfigBackendProvider. The default provider uses a ConfigBackendFactory to create a backend based on ConfigId (backend, feature).
+- For DataStore, see DefaultDataStoreConfigBackend and DataStoreConfigBackendFactory for reference.
+
+## Migration
+
+- **Trigger**: On first subscription, `BaseConfigStore` compares stored vs. definition version.
+
+- **Contract**: `ConfigMigration.migrate(currentVersion, newVersion, current: Config): ConfigMigrationResult` returns:
+  - `Migrated`(updated: Config, keysToRemove: Set<ConfigKey<*>>)
+  - `NoOp`
+- **Behavior**:
+  - `Migrated`: backend updates config, removes obsolete keys, writes new version.
+  - `NoOp`: only writes new version.
+- **Examples**:
+  - _Rename key_: copy old value to new key, mark old key for removal in keysToRemove.
+  - _Remove key_: omit from updated and include in keysToRemove.
+
+## Limitations
+
+- DataStore Preferences is a simple key-value store; there’s no schema enforcement beyond your key defintion.
+- Avoid **large values** because of their performance impact, especially storing blobs.
+- Keys are plain strings; renames require migrations to prevent orphaned values.
+- File name is `<backend>.preferences_pb`; all features using the same backend id share one preferences file.
diff --git a/core/configstore/api/build.gradle.kts b/core/configstore/api/build.gradle.kts
new file mode 100644
index 00000000000..250aa59bb02
--- /dev/null
+++ b/core/configstore/api/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+    id(ThunderbirdPlugins.Library.kmp)
+}
+
+android {
+    namespace = "net.thunderbird.core.configstore"
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/BaseConfigStore.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/BaseConfigStore.kt
new file mode 100644
index 00000000000..72be63f2680
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/BaseConfigStore.kt
@@ -0,0 +1,73 @@
+package net.thunderbird.core.configstore
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import net.thunderbird.core.configstore.backend.ConfigBackend
+import net.thunderbird.core.configstore.backend.ConfigBackendProvider
+
+/**
+ * Base class for configuration stores that provides common functionality for managing configuration data.
+ *
+ * @param T The type of configuration data.
+ * @param provider The [ConfigBackendProvider] that provides or creates the [ConfigBackend] for storing config data.
+ * @param definition The definition that describes how to map the configuration data to and from the backend.
+ */
+abstract class BaseConfigStore<T>(
+    private val provider: ConfigBackendProvider,
+    private val definition: ConfigDefinition<T>,
+) : ConfigStore<T> {
+
+    private var migrationPerformed = false
+
+    private val backend: ConfigBackend by lazy {
+        provider.provide(definition.id)
+    }
+
+    override val config: Flow<T> = backend.read(definition.keys)
+        .onStart { ensureMigration() }
+        .map { definition.mapper.fromConfig(it) ?: definition.defaultValue }
+        .distinctUntilChanged()
+
+    override suspend fun update(transform: (T?) -> T) {
+        backend.update(definition.keys) { config ->
+            val current = definition.mapper.fromConfig(config) ?: definition.defaultValue
+            definition.mapper.toConfig(transform(current))
+        }
+    }
+
+    override suspend fun clear() = backend.clear()
+
+    private fun getVersionKey(): String {
+        val id = definition.id
+        return "_version_${id.backend}_${id.feature}"
+    }
+
+    private suspend fun ensureMigration() {
+        if (migrationPerformed) return
+        val versionKey = getVersionKey()
+        val currentVersion = backend.readVersion(versionKey)
+
+        if (currentVersion < definition.version) {
+            val currentConfig = backend.read(definition.keys).first()
+            val result = definition.migration.migrate(currentVersion, definition.version, currentConfig)
+            when (result) {
+                is ConfigMigrationResult.Migrated -> {
+                    backend.update(definition.keys) { result.updated }
+                    backend.removeKeys(result.keysToRemove)
+                }
+
+                ConfigMigrationResult.NoOp -> {
+                    // No migration needed, just ensure the version is updated
+                }
+            }
+
+            // Write the new version to the backend
+            backend.writeVersion(versionKey, definition.version)
+
+            migrationPerformed = true
+        }
+    }
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/Config.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/Config.kt
new file mode 100644
index 00000000000..8186a7bfe8c
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/Config.kt
@@ -0,0 +1,22 @@
+package net.thunderbird.core.configstore
+
+/**
+ * A configuration holds key-value pairs of [ConfigKey] and their corresponding values.
+ *
+ * It is used to store and retrieve configuration settings in a type-safe manner.
+ */
+class Config(
+    private val entries: MutableMap<ConfigKey<*>, Any?> = mutableMapOf(),
+) {
+    @Suppress("UNCHECKED_CAST")
+    operator fun <T> get(key: ConfigKey<T>): T? = entries[key] as? T
+
+    operator fun <T> set(key: ConfigKey<T>, value: T) {
+        entries[key] = value
+    }
+
+    /**
+     * Returns a map representation of the configuration.
+     */
+    fun toMap(): Map<ConfigKey<*>, Any?> = entries.toMap()
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigDefinition.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigDefinition.kt
new file mode 100644
index 00000000000..6ba084c035b
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigDefinition.kt
@@ -0,0 +1,50 @@
+package net.thunderbird.core.configstore
+
+/**
+ * A definition of how to manage a configuration for specific type.
+ *
+ * The id of the configuration is used as a unique identifier to distinguish it from other configurations and
+ * is used by the [net.thunderbird.core.configstore.backend.ConfigBackendProvider] to retrieve the correct configuration backend.
+ *
+ * This allows configurations to selectively share a backend, which can be useful for performance or organizational purposes.
+ * It also allows for the creation of multiple configurations that can be managed independently, even if they share the same backend.
+ *
+ * @param T The type of the configuration object.
+ *
+ */
+interface ConfigDefinition<T> {
+
+    /**
+     * The version of the configuration.
+     */
+    val version: Int
+
+    /**
+     * The id of the configuration.
+     *
+     * It is used by the [net.thunderbird.core.configstore.backend.ConfigBackendProvider] to retrieve the correct configuration backend.
+     */
+    val id: ConfigId
+
+    /**
+     * The mapper used to convert between the configuration object and its representation in the config store.
+     */
+    val mapper: ConfigMapper<T>
+
+    /**
+     * The default value for the configuration.
+     */
+    val defaultValue: T
+
+    /**
+     * The list of keys that define the configuration.
+     *
+     * These keys are used to store and retrieve values in the configuration store.
+     */
+    val keys: List<ConfigKey<*>>
+
+    /**
+     * The migration strategy for the configuration.
+     */
+    val migration: ConfigMigration
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigId.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigId.kt
new file mode 100644
index 00000000000..cd428ab2fa9
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigId.kt
@@ -0,0 +1,18 @@
+package net.thunderbird.core.configstore
+
+/**
+ * Represents a unique identifier for a configuration.
+ *
+ * @property backend The backend identifier for the configuration.
+ * @property feature The feature identifier for the configuration.
+ * @throws IllegalArgumentException if the values are blank or contain invalid characters.
+ */
+data class ConfigId(
+    val backend: String,
+    val feature: String,
+) {
+    init {
+        require(backend.matches(Regex("^[a-zA-Z0-9_]+$"))) { "Invalid backend name: $backend" }
+        require(feature.matches(Regex("^[a-zA-Z0-9_]+$"))) { "Invalid feature name: $feature" }
+    }
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigKey.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigKey.kt
new file mode 100644
index 00000000000..4ef284abd6c
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigKey.kt
@@ -0,0 +1,19 @@
+package net.thunderbird.core.configstore
+
+/**
+ * Represents a key for a configuration value.
+ *
+ * This sealed class defines different types of configuration keys that can be used
+ * to store and retrieve values in a configuration store.
+ *
+ * @param T The type of the value associated with this key.
+ * @property name The name of the configuration key.
+ */
+sealed class ConfigKey<T>(val name: String) {
+    class BooleanKey(name: String) : ConfigKey<Boolean>(name)
+    class IntKey(name: String) : ConfigKey<Int>(name)
+    class StringKey(name: String) : ConfigKey<String>(name)
+    class LongKey(name: String) : ConfigKey<Long>(name)
+    class FloatKey(name: String) : ConfigKey<Float>(name)
+    class DoubleKey(name: String) : ConfigKey<Double>(name)
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigMapper.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigMapper.kt
new file mode 100644
index 00000000000..173c4f413af
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigMapper.kt
@@ -0,0 +1,24 @@
+package net.thunderbird.core.configstore
+
+/**
+ * Interface for mapping configuration objects to a [Config] representation and vice versa.
+ *
+ * @param T The type of the configuration object.
+ */
+interface ConfigMapper<T> {
+    /**
+     * Maps a configuration object to a [Config] representation.
+     *
+     * @param obj The configuration object to map.
+     * @return The [Config] representation of the configuration.
+     */
+    fun toConfig(obj: T): Config
+
+    /**
+     * Maps a string representation back to a configuration object.
+     *
+     * @param config The [Config] to map from.
+     * @return The configuration object.
+     */
+    fun fromConfig(config: Config): T?
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigMigration.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigMigration.kt
new file mode 100644
index 00000000000..85a476694d1
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigMigration.kt
@@ -0,0 +1,32 @@
+package net.thunderbird.core.configstore
+
+interface ConfigMigration {
+
+    /**
+     * Migrate the configuration to a new version.
+     *
+     * @param currentVersion The current version of the configuration.
+     * @param newVersion The new version to migrate to.
+     * @param current The current configuration to be migrated.
+     * @return A [ConfigMigrationResult] containing the updated configuration and any keys that should be removed.
+     */
+    suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult
+}
+
+sealed interface ConfigMigrationResult {
+    /**
+     * Result of a configuration migration, indicating the updated configuration and any keys that should be removed.
+     *
+     * @param updated The updated configuration after migration.
+     * @param keysToRemove A set of configuration keys that should be removed after migration.
+     */
+    data class Migrated(
+        val updated: Config,
+        val keysToRemove: Set<ConfigKey<*>> = emptySet(),
+    ) : ConfigMigrationResult
+
+    /**
+     * A no-op migration that does not change the configuration.
+     */
+    object NoOp : ConfigMigrationResult
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigStore.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigStore.kt
new file mode 100644
index 00000000000..93930721b58
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/ConfigStore.kt
@@ -0,0 +1,30 @@
+package net.thunderbird.core.configstore
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A generic interface for a configuration store that manages a configuration of type [T].
+ *
+ * @param T The type of the configuration stored in the config store.
+ */
+interface ConfigStore<T> {
+
+    /**
+     * The configuration stored in the config store, represented as a Flow.
+     *
+     * @param T The type of the configuration.
+     */
+    val config: Flow<T>
+
+    /**
+     * Updates the stored configuration by applying the provided transformation function
+     *
+     * @param transform A function that takes the current configuration and returns a new configuration.
+     */
+    suspend fun update(transform: (T?) -> T)
+
+    /**
+     * Clears the stored configuration.
+     */
+    suspend fun clear()
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackend.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackend.kt
new file mode 100644
index 00000000000..9d1176afe8d
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackend.kt
@@ -0,0 +1,40 @@
+package net.thunderbird.core.configstore.backend
+
+import kotlinx.coroutines.flow.Flow
+import net.thunderbird.core.configstore.Config
+import net.thunderbird.core.configstore.ConfigKey
+
+/**
+ * A config backend that is used to store and retrieve configuration data.
+ */
+interface ConfigBackend {
+
+    /**
+     * Reads the stored configuration from the backend.
+     *
+     * @param keys The list of configuration keys to read.
+     * @return A flow that emits the current configuration.
+     */
+    fun read(keys: List<ConfigKey<*>>): Flow<Config>
+
+    /**
+     * Writes the configuration to the backend.
+     *
+     * @param keys The list of configuration keys to write.
+     * @param transform A function that transforms the current configuration into a new configuration.
+     */
+    suspend fun update(
+        keys: List<ConfigKey<*>>,
+        transform: (Config) -> Config,
+    )
+
+    /**
+     * Clears the stored configuration.
+     */
+    suspend fun clear()
+
+    suspend fun readVersion(versionKey: String): Int
+    suspend fun writeVersion(versionKey: String, version: Int)
+
+    suspend fun removeKeys(keys: Set<ConfigKey<*>>)
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendFactory.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendFactory.kt
new file mode 100644
index 00000000000..26630f599bd
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendFactory.kt
@@ -0,0 +1,16 @@
+package net.thunderbird.core.configstore.backend
+
+import net.thunderbird.core.configstore.ConfigId
+
+/**
+ * Factory interface for creating [ConfigBackend] instances.
+ */
+interface ConfigBackendFactory {
+    /**
+     * Creates a [ConfigBackend].
+     *
+     * @param id The [ConfigId] for used to identify the [ConfigBackend].
+     * @return A [ConfigBackend] instance.
+     */
+    fun create(id: ConfigId): ConfigBackend
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendProvider.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendProvider.kt
new file mode 100644
index 00000000000..e88c201ce0a
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendProvider.kt
@@ -0,0 +1,16 @@
+package net.thunderbird.core.configstore.backend
+
+import net.thunderbird.core.configstore.ConfigId
+
+/**
+ * Provider that is responsible for providing a [ConfigBackend] by its [ConfigId].
+ */
+interface ConfigBackendProvider {
+
+    /**
+     * Provides a [ConfigBackend] instance for the given [ConfigId].
+     *
+     * @param id The id of the [ConfigBackend] to provide.
+     */
+    fun provide(id: ConfigId): ConfigBackend
+}
diff --git a/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DefaultConfigBackendProvider.kt b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DefaultConfigBackendProvider.kt
new file mode 100644
index 00000000000..f1e7119c663
--- /dev/null
+++ b/core/configstore/api/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DefaultConfigBackendProvider.kt
@@ -0,0 +1,23 @@
+package net.thunderbird.core.configstore.backend
+
+import net.thunderbird.core.configstore.ConfigId
+
+/**
+ * Default implementation of [ConfigBackendProvider] that uses a [ConfigBackendFactory]
+ * to create and cache [ConfigBackend] instances.
+ */
+class DefaultConfigBackendProvider(
+    private val backendFactory: ConfigBackendFactory,
+    private val backends: MutableMap<ConfigId, ConfigBackend> = mutableMapOf(),
+) : ConfigBackendProvider {
+
+    override fun provide(id: ConfigId): ConfigBackend {
+        return backends[id] ?: createAndCacheBackend(id)
+    }
+
+    private fun createAndCacheBackend(id: ConfigId): ConfigBackend {
+        val backend = backendFactory.create(id)
+        backends[id] = backend
+        return backend
+    }
+}
diff --git a/core/configstore/impl-backend/build.gradle.kts b/core/configstore/impl-backend/build.gradle.kts
new file mode 100644
index 00000000000..23ee8867bf0
--- /dev/null
+++ b/core/configstore/impl-backend/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+    id(ThunderbirdPlugins.Library.kmp)
+}
+
+android {
+    namespace = "net.thunderbird.core.configstore.backend"
+}
+
+kotlin {
+    sourceSets {
+        commonMain.dependencies {
+            api(projects.core.configstore.api)
+            implementation(projects.core.logging.api)
+
+            implementation(libs.androidx.datastore.preferences)
+        }
+
+        commonTest.dependencies {
+            implementation(projects.core.testing)
+            implementation(libs.assertk)
+        }
+
+        jvmTest.dependencies {
+            implementation(libs.junit)
+        }
+    }
+}
diff --git a/core/configstore/impl-backend/src/androidMain/kotlin/net/thunderbird/core/configstore/backend/AndroidConfigBackendFileManager.kt b/core/configstore/impl-backend/src/androidMain/kotlin/net/thunderbird/core/configstore/backend/AndroidConfigBackendFileManager.kt
new file mode 100644
index 00000000000..58cb3cbb6c1
--- /dev/null
+++ b/core/configstore/impl-backend/src/androidMain/kotlin/net/thunderbird/core/configstore/backend/AndroidConfigBackendFileManager.kt
@@ -0,0 +1,16 @@
+package net.thunderbird.core.configstore.backend
+
+import android.content.Context
+
+/**
+ * Android implementation of [ConfigBackendFileManager] that provides file paths for configuration backends.
+ *
+ * @param context The Android context used to access application-specific files.
+ */
+class AndroidConfigBackendFileManager(
+    val context: Context,
+) : ConfigBackendFileManager {
+    override fun getFilePath(backendFileName: String): String {
+        return context.filesDir.resolve(backendFileName).absolutePath
+    }
+}
diff --git a/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendFileManager.kt b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendFileManager.kt
new file mode 100644
index 00000000000..374e9cf6d38
--- /dev/null
+++ b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/ConfigBackendFileManager.kt
@@ -0,0 +1,17 @@
+package net.thunderbird.core.configstore.backend
+
+/**
+ * Interface for managing file paths for configuration backends.
+ *
+ * This interface provides a method to retrieve the file path for a given backend file name.
+ */
+interface ConfigBackendFileManager {
+
+    /**
+     * Retrieves the file path for a given backend file name.
+     *
+     * @param backendFileName The name of the backend file for which to retrieve the path.
+     * @return The file path as a [String].
+     */
+    fun getFilePath(backendFileName: String): String
+}
diff --git a/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackend.kt b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackend.kt
new file mode 100644
index 00000000000..3801e268418
--- /dev/null
+++ b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackend.kt
@@ -0,0 +1,6 @@
+package net.thunderbird.core.configstore.backend
+
+/**
+ * Interface for a data store configuration backend that provides access to configuration data.
+ */
+interface DataStoreConfigBackend : ConfigBackend
diff --git a/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackendFactory.kt b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackendFactory.kt
new file mode 100644
index 00000000000..f3e72c65052
--- /dev/null
+++ b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackendFactory.kt
@@ -0,0 +1,26 @@
+package net.thunderbird.core.configstore.backend
+
+import androidx.datastore.preferences.core.PreferenceDataStoreFactory
+import net.thunderbird.core.configstore.ConfigId
+import net.thunderbird.core.configstore.backend.DefaultDataStoreConfigBackend
+import okio.Path.Companion.toPath
+
+internal const val DATA_STORE_FILE_EXTENSION = "preferences_pb"
+
+class DataStoreConfigBackendFactory(
+    private val fileManager: ConfigBackendFileManager,
+) : ConfigBackendFactory {
+    override fun create(id: ConfigId): ConfigBackend {
+        val dataStore = PreferenceDataStoreFactory.createWithPath(
+            produceFile = {
+                fileManager.getFilePath(generateFileName(id)).toPath()
+            },
+        )
+
+        return DefaultDataStoreConfigBackend(dataStore)
+    }
+
+    private fun generateFileName(id: ConfigId): String {
+        return "${id.backend}.$DATA_STORE_FILE_EXTENSION"
+    }
+}
diff --git a/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DefaultDataStoreConfigBackend.kt b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DefaultDataStoreConfigBackend.kt
new file mode 100644
index 00000000000..a10c84ff3fd
--- /dev/null
+++ b/core/configstore/impl-backend/src/commonMain/kotlin/net/thunderbird/core/configstore/backend/DefaultDataStoreConfigBackend.kt
@@ -0,0 +1,95 @@
+package net.thunderbird.core.configstore.backend
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.doublePreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.floatPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import kotlin.collections.iterator
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import net.thunderbird.core.configstore.Config
+import net.thunderbird.core.configstore.ConfigKey
+
+/**
+ * A low-level implementation of a configuration store backed by DataStore Preferences.
+ * This class is responsible for serialization, deserialization, and persistence.
+ */
+class DefaultDataStoreConfigBackend(
+    private val dataStore: DataStore<Preferences>,
+) : DataStoreConfigBackend {
+
+    override fun read(keys: List<ConfigKey<*>>): Flow<Config> = dataStore.data.map { preferences ->
+        val config = Config()
+        keys.forEach { key ->
+            when (key) {
+                is ConfigKey.BooleanKey -> preferences[booleanPreferencesKey(key.name)]?.let { config[key] = it }
+                is ConfigKey.IntKey -> preferences[intPreferencesKey(key.name)]?.let { config[key] = it }
+                is ConfigKey.StringKey -> preferences[stringPreferencesKey(key.name)]?.let { config[key] = it }
+                is ConfigKey.LongKey -> preferences[longPreferencesKey(key.name)]?.let { config[key] = it }
+                is ConfigKey.FloatKey -> preferences[floatPreferencesKey(key.name)]?.let { config[key] = it }
+                is ConfigKey.DoubleKey -> preferences[doublePreferencesKey(key.name)]?.let { config[key] = it }
+            }
+        }
+        config
+    }
+
+    override suspend fun update(keys: List<ConfigKey<*>>, transform: (Config) -> Config) {
+        val current = read(keys).first()
+        val updated = transform(current)
+
+        if (keys.isEmpty()) return
+
+        dataStore.edit { preferences ->
+            for ((key, value) in updated.toMap()) {
+                when (key) {
+                    is ConfigKey.BooleanKey -> if (value is Boolean) {
+                        preferences[booleanPreferencesKey(key.name)] = value
+                    }
+
+                    is ConfigKey.IntKey -> if (value is Int) preferences[intPreferencesKey(key.name)] = value
+                    is ConfigKey.StringKey -> if (value is String) preferences[stringPreferencesKey(key.name)] = value
+                    is ConfigKey.LongKey -> if (value is Long) preferences[longPreferencesKey(key.name)] = value
+                    is ConfigKey.FloatKey -> if (value is Float) preferences[floatPreferencesKey(key.name)] = value
+                    is ConfigKey.DoubleKey -> if (value is Double) preferences[doublePreferencesKey(key.name)] = value
+                }
+            }
+        }
+    }
+
+    override suspend fun clear() {
+        dataStore.edit { it.clear() }
+    }
+
+    override suspend fun readVersion(versionKey: String): Int {
+        return dataStore.data.map { preferences ->
+            preferences[intPreferencesKey(versionKey)] ?: 0
+        }.first()
+    }
+
+    override suspend fun writeVersion(versionKey: String, version: Int) {
+        dataStore.edit { preferences ->
+            preferences[intPreferencesKey(versionKey)] = version
+        }
+    }
+
+    override suspend fun removeKeys(keys: Set<ConfigKey<*>>) {
+        dataStore.edit { preferences ->
+            for (key in keys) {
+                when (key) {
+                    is ConfigKey.BooleanKey -> preferences.remove(booleanPreferencesKey(key.name))
+                    is ConfigKey.IntKey -> preferences.remove(intPreferencesKey(key.name))
+                    is ConfigKey.StringKey -> preferences.remove(stringPreferencesKey(key.name))
+                    is ConfigKey.LongKey -> preferences.remove(longPreferencesKey(key.name))
+                    is ConfigKey.FloatKey -> preferences.remove(floatPreferencesKey(key.name))
+                    is ConfigKey.DoubleKey -> preferences.remove(doublePreferencesKey(key.name))
+                }
+            }
+        }
+    }
+}
diff --git a/core/configstore/impl-backend/src/jvmMain/kotlin/net/thunderbird/core/configstore/backend/JvmConfigBackendFileManager.kt b/core/configstore/impl-backend/src/jvmMain/kotlin/net/thunderbird/core/configstore/backend/JvmConfigBackendFileManager.kt
new file mode 100644
index 00000000000..cbedc18806c
--- /dev/null
+++ b/core/configstore/impl-backend/src/jvmMain/kotlin/net/thunderbird/core/configstore/backend/JvmConfigBackendFileManager.kt
@@ -0,0 +1,18 @@
+package net.thunderbird.core.configstore.backend
+
+import java.io.File
+
+/**
+ * Implementation of [ConfigBackendFileManager] for JVM platforms.
+ *
+ * This class provides a way to manage file paths for configuration backends in a JVM environment.
+ *
+ * @property workDirectory The directory where backend files are stored.
+ */
+class JvmConfigBackendFileManager(
+    private val workDirectory: File,
+) : ConfigBackendFileManager {
+    override fun getFilePath(backendFileName: String): String {
+        return File(workDirectory, backendFileName).absolutePath
+    }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 12d89abd31a..79c0a6cb368 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -30,6 +30,7 @@ androidxConstraintLayout = "2.2.1"
 androidxCoordinatorLayout = "1.3.0"
 androidxCore = "1.16.0"
 androidxCoreSplashscreen = "1.0.1"
+androidxDatastore = "1.1.7"
 androidxFragment = "1.8.8"
 androidxGlance = "1.1.1"
 androidxGlanceMaterial3 = "1.1.1"
@@ -165,6 +166,8 @@ androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorl
 androidx-core = { module = "androidx.core:core", version.ref = "androidxCore" }
 androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
 androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidxCoreSplashscreen" }
+androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "androidxDatastore" }
+androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "androidxDatastore" }
 androidx-fragment = { module = "androidx.fragment:fragment", version.ref = "androidxFragment" }
 androidx-fragment-compose = { module = "androidx.fragment:fragment-compose", version.ref = "androidxFragment" }
 androidx-fragment-testing = { module = "androidx.fragment:fragment-testing", version.ref = "androidxFragment" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 141ff5bd097..35fb8a7bd7c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -149,6 +149,8 @@ include(
 include(
     ":core:architecture:api",
     ":core:common",
+    ":core:configstore:api",
+    ":core:configstore:impl-backend",
     ":core:featureflag",
     ":core:logging:api",
     ":core:logging:impl-composite",
@@ -252,7 +254,7 @@ include(
 check(JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_17)) {
     """
         Java 17+ is required to build Thunderbird for Android.
-        But it found an incompatible Java version ${{JavaVersion.current()}}.
+        But it found an incompatible Java version ${{ JavaVersion.current() }}.
 
         Java Home: [${System.getProperty("java.home")}]
 


Test patch:
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/BaseConfigStoreMigrationTest.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/BaseConfigStoreMigrationTest.kt
new file mode 100644
index 00000000000..df180bab47e
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/BaseConfigStoreMigrationTest.kt
@@ -0,0 +1,168 @@
+package net.thunderbird.core.configstore
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isTrue
+import kotlin.test.Test
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+
+class BaseConfigStoreMigrationTest {
+
+    @Test
+    fun `should apply migration when version is updated`() = runTest {
+        // Arrange
+        val initialConfig = TestConfig("initial", 100)
+        val migratedConfig = TestConfig("migrated", 200)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+
+        // Create a test migration that updates the config
+        val migration = object : ConfigMigration {
+            override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult {
+                val updated = Config().apply {
+                    this[mapper.stringKey] = migratedConfig.stringValue
+                    this[mapper.intKey] = migratedConfig.intValue
+                }
+                return ConfigMigrationResult.Migrated(updated)
+            }
+        }
+
+        // Create a config definition with version 2 and our test migration
+        val definition = TestConfigDefinition(mapper, initialConfig, version = 2, migration = migration)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Set up backend to return initial config with version 1
+        val initialConfigData = Config().apply {
+            this[mapper.stringKey] = initialConfig.stringValue
+            this[mapper.intKey] = initialConfig.intValue
+        }
+        backend.setConfig(initialConfigData)
+
+        // Act
+        val result = testSubject.config.first()
+
+        // Assert
+        assertThat(result.stringValue).isEqualTo(migratedConfig.stringValue)
+        assertThat(result.intValue).isEqualTo(migratedConfig.intValue)
+
+        // Verify version was updated
+        val versionKey = "_version_test_backend_test_feature"
+        assertThat(backend.readVersion(versionKey)).isEqualTo(2)
+    }
+
+    @Test
+    fun `should remove keys when migration specifies keys to remove`() = runTest {
+        // Arrange
+        val initialConfig = TestConfig("initial", 100)
+        val migratedConfig = TestConfig("migrated", 200)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+
+        // Create a test migration that updates the config and removes keys
+        val migration = object : ConfigMigration {
+            override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult {
+                val updated = Config().apply {
+                    this[mapper.stringKey] = migratedConfig.stringValue
+                    this[mapper.intKey] = migratedConfig.intValue
+                }
+                return ConfigMigrationResult.Migrated(updated, setOf(mapper.intKey))
+            }
+        }
+
+        // Create a config definition with version 2 and our test migration
+        val definition = TestConfigDefinition(mapper, initialConfig, version = 2, migration = migration)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Set up backend to return initial config with version 1
+        val initialConfigData = Config().apply {
+            this[mapper.stringKey] = initialConfig.stringValue
+            this[mapper.intKey] = initialConfig.intValue
+        }
+        backend.setConfig(initialConfigData)
+
+        // Act
+        testSubject.config.first() // Trigger migration
+
+        // Assert
+        assertThat(backend.removedKeys.contains(mapper.intKey)).isTrue()
+    }
+
+    @Test
+    fun `should not apply migration when version is current`() = runTest {
+        // Arrange
+        val initialConfig = TestConfig("initial", 100)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+
+        // Create a test migration that should not be called
+        var migrationCalled = false
+        val migration = object : ConfigMigration {
+            override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult {
+                migrationCalled = true
+                return ConfigMigrationResult.NoOp
+            }
+        }
+
+        // Create a config definition with version 1 and our test migration
+        val definition = TestConfigDefinition(mapper, initialConfig, version = 1, migration = migration)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Set up backend to return initial config with version 1
+        val initialConfigData = Config().apply {
+            this[mapper.stringKey] = initialConfig.stringValue
+            this[mapper.intKey] = initialConfig.intValue
+        }
+        backend.setConfig(initialConfigData)
+        backend.writeVersion("_version_test_backend_test_feature", 1)
+
+        // Act
+        val result = testSubject.config.first()
+
+        // Assert
+        assertThat(result.stringValue).isEqualTo(initialConfig.stringValue)
+        assertThat(result.intValue).isEqualTo(initialConfig.intValue)
+        assertThat(migrationCalled).isEqualTo(false)
+    }
+
+    @Test
+    fun `should apply NoOp migration when specified`() = runTest {
+        // Arrange
+        val initialConfig = TestConfig("initial", 100)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+
+        // Create a test migration that returns NoOp
+        val migration = object : ConfigMigration {
+            override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult {
+                return ConfigMigrationResult.NoOp
+            }
+        }
+
+        // Create a config definition with version 2 and our test migration
+        val definition = TestConfigDefinition(mapper, initialConfig, version = 2, migration = migration)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Set up backend to return initial config with version 1
+        val initialConfigData = Config().apply {
+            this[mapper.stringKey] = initialConfig.stringValue
+            this[mapper.intKey] = initialConfig.intValue
+        }
+        backend.setConfig(initialConfigData)
+
+        // Act
+        val result = testSubject.config.first()
+
+        // Assert
+        assertThat(result.stringValue).isEqualTo(initialConfig.stringValue)
+        assertThat(result.intValue).isEqualTo(initialConfig.intValue)
+
+        // Verify version was updated even though no migration was performed
+        val versionKey = "_version_test_backend_test_feature"
+        assertThat(backend.readVersion(versionKey)).isEqualTo(2)
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/BaseConfigStoreTest.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/BaseConfigStoreTest.kt
new file mode 100644
index 00000000000..9c9e29dc2cc
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/BaseConfigStoreTest.kt
@@ -0,0 +1,79 @@
+package net.thunderbird.core.configstore
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import kotlin.test.Test
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+
+class BaseConfigStoreTest {
+
+    @Test
+    fun `config should return mapped value from backend`() = runTest {
+        // Arrange
+        val testConfig = TestConfig("test value", 123)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+        val definition = TestConfigDefinition(mapper, testConfig)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Set up backend to return a config
+        val config = Config().apply {
+            this[mapper.stringKey] = testConfig.stringValue
+            this[mapper.intKey] = testConfig.intValue
+        }
+        backend.setConfig(config)
+
+        // Act
+        val result = testSubject.config.first()
+
+        // Assert
+        assertThat(result?.stringValue).isEqualTo(testConfig.stringValue)
+        assertThat(result?.intValue).isEqualTo(testConfig.intValue)
+    }
+
+    @Test
+    fun `update should transform the config and store it in backend`() = runTest {
+        // Arrange
+        val initialConfig = TestConfig("initial", 100)
+        val updatedConfig = TestConfig("updated", 200)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+        val definition = TestConfigDefinition(mapper, initialConfig)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Set up backend to return initial config
+        val initialConfigData = Config().apply {
+            this[mapper.stringKey] = initialConfig.stringValue
+            this[mapper.intKey] = initialConfig.intValue
+        }
+        backend.setConfig(initialConfigData)
+
+        // Act
+        testSubject.update { _ -> updatedConfig }
+
+        // Assert
+        val storedConfig = backend.getLastStoredConfig()
+        assertThat(storedConfig[mapper.stringKey]).isEqualTo(updatedConfig.stringValue)
+        assertThat(storedConfig[mapper.intKey]).isEqualTo(updatedConfig.intValue)
+    }
+
+    @Test
+    fun `clear should clear the backend`() = runTest {
+        // Arrange
+        val testConfig = TestConfig("test value", 123)
+        val backend = FakeConfigBackend()
+        val provider = FakeConfigBackendProvider(backend)
+        val mapper = TestConfigMapper()
+        val definition = TestConfigDefinition(mapper, testConfig)
+        val testSubject = TestConfigStore(provider, definition)
+
+        // Act
+        testSubject.clear()
+
+        // Assert
+        assertThat(backend.wasCleared).isEqualTo(true)
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigIdTest.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigIdTest.kt
new file mode 100644
index 00000000000..1c71051d9d5
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigIdTest.kt
@@ -0,0 +1,85 @@
+package net.thunderbird.core.configstore
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import kotlin.test.Test
+import kotlin.test.assertFailsWith
+
+class ConfigIdTest {
+
+    @Test
+    fun `should create ConfigId with valid backend and feature`() {
+        // Arrange
+        val backend = "valid_backend"
+        val feature = "valid_feature"
+
+        // Act
+        val configId = ConfigId(backend, feature)
+
+        // Assert
+        assertThat(configId.backend).isEqualTo(backend)
+        assertThat(configId.feature).isEqualTo(feature)
+    }
+
+    @Test
+    fun `should throw IllegalArgumentException when backend is blank`() {
+        // Arrange
+        val backend = ""
+        val feature = "valid_feature"
+
+        // Act & Assert
+        assertFailsWith<IllegalArgumentException> {
+            ConfigId(backend, feature)
+        }
+    }
+
+    @Test
+    fun `should throw IllegalArgumentException when feature is blank`() {
+        // Arrange
+        val backend = "valid_backend"
+        val feature = ""
+
+        // Act & Assert
+        assertFailsWith<IllegalArgumentException> {
+            ConfigId(backend, feature)
+        }
+    }
+
+    @Test
+    fun `should throw IllegalArgumentException when backend contains invalid characters`() {
+        // Arrange
+        val backend = "invalid-backend"
+        val feature = "valid_feature"
+
+        // Act & Assert
+        assertFailsWith<IllegalArgumentException> {
+            ConfigId(backend, feature)
+        }
+    }
+
+    @Test
+    fun `should throw IllegalArgumentException when feature contains invalid characters`() {
+        // Arrange
+        val backend = "valid_backend"
+        val feature = "invalid-feature"
+
+        // Act & Assert
+        assertFailsWith<IllegalArgumentException> {
+            ConfigId(backend, feature)
+        }
+    }
+
+    @Test
+    fun `should allow alphanumeric characters and underscores in backend and feature`() {
+        // Arrange
+        val backend = "valid_backend_123"
+        val feature = "valid_feature_456"
+
+        // Act
+        val configId = ConfigId(backend, feature)
+
+        // Assert
+        assertThat(configId.backend).isEqualTo(backend)
+        assertThat(configId.feature).isEqualTo(feature)
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigKeyTest.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigKeyTest.kt
new file mode 100644
index 00000000000..bf9ad9f505c
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigKeyTest.kt
@@ -0,0 +1,87 @@
+package net.thunderbird.core.configstore
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
+import kotlin.test.Test
+
+class ConfigKeyTest {
+
+    @Test
+    fun `BooleanKey should have correct name and type`() {
+        // Arrange
+        val name = "booleanKey"
+
+        // Act
+        val key = ConfigKey.BooleanKey(name)
+
+        // Assert
+        assertThat(key.name).isEqualTo(name)
+        assertThat(key).isInstanceOf(ConfigKey.BooleanKey::class)
+    }
+
+    @Test
+    fun `IntKey should have correct name and type`() {
+        // Arrange
+        val name = "intKey"
+
+        // Act
+        val key = ConfigKey.IntKey(name)
+
+        // Assert
+        assertThat(key.name).isEqualTo(name)
+        assertThat(key).isInstanceOf(ConfigKey.IntKey::class)
+    }
+
+    @Test
+    fun `StringKey should have correct name and type`() {
+        // Arrange
+        val name = "stringKey"
+
+        // Act
+        val key = ConfigKey.StringKey(name)
+
+        // Assert
+        assertThat(key.name).isEqualTo(name)
+        assertThat(key).isInstanceOf(ConfigKey.StringKey::class)
+    }
+
+    @Test
+    fun `LongKey should have correct name and type`() {
+        // Arrange
+        val name = "longKey"
+
+        // Act
+        val key = ConfigKey.LongKey(name)
+
+        // Assert
+        assertThat(key.name).isEqualTo(name)
+        assertThat(key).isInstanceOf(ConfigKey.LongKey::class)
+    }
+
+    @Test
+    fun `FloatKey should have correct name and type`() {
+        // Arrange
+        val name = "floatKey"
+
+        // Act
+        val key = ConfigKey.FloatKey(name)
+
+        // Assert
+        assertThat(key.name).isEqualTo(name)
+        assertThat(key).isInstanceOf(ConfigKey.FloatKey::class)
+    }
+
+    @Test
+    fun `DoubleKey should have correct name and type`() {
+        // Arrange
+        val name = "doubleKey"
+
+        // Act
+        val key = ConfigKey.DoubleKey(name)
+
+        // Assert
+        assertThat(key.name).isEqualTo(name)
+        assertThat(key).isInstanceOf(ConfigKey.DoubleKey::class)
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigMapperTest.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigMapperTest.kt
new file mode 100644
index 00000000000..1b9b8336bd5
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigMapperTest.kt
@@ -0,0 +1,56 @@
+package net.thunderbird.core.configstore
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNull
+import kotlin.test.Test
+
+class ConfigMapperTest {
+
+    @Test
+    fun `toConfig should map object to Config`() {
+        // Arrange
+        val testSubject = TestConfigMapper()
+        val testObject = TestConfig("test value", 123)
+
+        // Act
+        val result = testSubject.toConfig(testObject)
+
+        // Assert
+        assertThat(result[testSubject.stringKey]).isEqualTo(testObject.stringValue)
+        assertThat(result[testSubject.intKey]).isEqualTo(testObject.intValue)
+    }
+
+    @Test
+    fun `fromConfig should map Config to object`() {
+        // Arrange
+        val testSubject = TestConfigMapper()
+        val config = Config().apply {
+            this[testSubject.stringKey] = "test value"
+            this[testSubject.intKey] = 123
+        }
+
+        // Act
+        val result = testSubject.fromConfig(config)
+
+        // Assert
+        assertThat(result?.stringValue).isEqualTo("test value")
+        assertThat(result?.intValue).isEqualTo(123)
+    }
+
+    @Test
+    fun `fromConfig should return null when required values are missing`() {
+        // Arrange
+        val testSubject = TestConfigMapper()
+        val config = Config().apply {
+            // Missing stringKey value
+            this[testSubject.intKey] = 123
+        }
+
+        // Act
+        val result = testSubject.fromConfig(config)
+
+        // Assert
+        assertThat(result).isNull()
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigTest.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigTest.kt
new file mode 100644
index 00000000000..3c36b6eb34b
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/ConfigTest.kt
@@ -0,0 +1,100 @@
+package net.thunderbird.core.configstore
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNull
+import kotlin.test.Test
+
+class ConfigTest {
+
+    @Test
+    fun `should return null when key is not set`() {
+        // Arrange
+        val config = Config()
+        val key = ConfigKey.StringKey("test")
+
+        // Act
+        val result = config[key]
+
+        // Assert
+        assertThat(result).isNull()
+    }
+
+    @Test
+    fun `should return value when key is set`() {
+        // Arrange
+        val config = Config()
+        val key = ConfigKey.StringKey("test")
+        val value = "value"
+        config[key] = value
+
+        // Act
+        val result = config[key]
+
+        // Assert
+        assertThat(result).isEqualTo(value)
+    }
+
+    @Test
+    fun `should update value when key is set`() {
+        // Arrange
+        val config = Config()
+        val key = ConfigKey.StringKey("test")
+        val initialValue = "initial"
+        val updatedValue = "updated"
+        config[key] = initialValue
+
+        // Act
+        config[key] = updatedValue
+        val result = config[key]
+
+        // Assert
+        assertThat(result).isEqualTo(updatedValue)
+    }
+
+    @Test
+    fun `should handle different key types`() {
+        // Arrange
+        val config = Config()
+        val stringKey = ConfigKey.StringKey("string")
+        val intKey = ConfigKey.IntKey("int")
+        val booleanKey = ConfigKey.BooleanKey("boolean")
+        val longKey = ConfigKey.LongKey("long")
+        val floatKey = ConfigKey.FloatKey("float")
+        val doubleKey = ConfigKey.DoubleKey("double")
+
+        // Act
+        config[stringKey] = "string value"
+        config[intKey] = 123
+        config[booleanKey] = true
+        config[longKey] = 123L
+        config[floatKey] = 123.45f
+        config[doubleKey] = 123.45
+
+        // Assert
+        assertThat(config[stringKey]).isEqualTo("string value")
+        assertThat(config[intKey]).isEqualTo(123)
+        assertThat(config[booleanKey]).isEqualTo(true)
+        assertThat(config[longKey]).isEqualTo(123L)
+        assertThat(config[floatKey]).isEqualTo(123.45f)
+        assertThat(config[doubleKey]).isEqualTo(123.45)
+    }
+
+    @Test
+    fun `toMap should return a map of all entries`() {
+        // Arrange
+        val config = Config()
+        val key1 = ConfigKey.StringKey("key1")
+        val key2 = ConfigKey.IntKey("key2")
+        config[key1] = "value1"
+        config[key2] = 2
+
+        // Act
+        val map = config.toMap()
+
+        // Assert
+        assertThat(map.size).isEqualTo(2)
+        assertThat(map[key1]).isEqualTo("value1")
+        assertThat(map[key2]).isEqualTo(2)
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/FakeConfigBackend.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/FakeConfigBackend.kt
new file mode 100644
index 00000000000..f39caae966c
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/FakeConfigBackend.kt
@@ -0,0 +1,51 @@
+package net.thunderbird.core.configstore
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import net.thunderbird.core.configstore.backend.ConfigBackend
+
+internal class FakeConfigBackend : ConfigBackend {
+    private val configFlow = MutableStateFlow(Config())
+    private var lastStoredConfig: Config? = null
+    private val versionMap = mutableMapOf<String, Int>()
+    var wasCleared = false
+    var removedKeys = mutableSetOf<ConfigKey<*>>()
+
+    fun setConfig(config: Config) {
+        configFlow.value = config
+    }
+
+    fun getLastStoredConfig(): Config {
+        return lastStoredConfig ?: Config()
+    }
+
+    override fun read(keys: List<ConfigKey<*>>): Flow<Config> = configFlow
+
+    override suspend fun update(keys: List<ConfigKey<*>>, transform: (Config) -> Config) {
+        val newConfig = transform(configFlow.value)
+        configFlow.value = newConfig
+        lastStoredConfig = newConfig
+    }
+
+    override suspend fun clear() {
+        configFlow.value = Config()
+        versionMap.clear()
+        wasCleared = true
+    }
+
+    override suspend fun readVersion(versionKey: String): Int {
+        return versionMap[versionKey] ?: 0
+    }
+
+    override suspend fun writeVersion(versionKey: String, version: Int) {
+        versionMap[versionKey] = version
+    }
+
+    override suspend fun removeKeys(keys: Set<ConfigKey<*>>) {
+        val currentConfig = configFlow.value
+        val entries = currentConfig.toMap().toMutableMap()
+        keys.forEach { key -> entries.remove(key) }
+        configFlow.value = Config(entries)
+        removedKeys.addAll(keys)
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/FakeConfigBackendProvider.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/FakeConfigBackendProvider.kt
new file mode 100644
index 00000000000..972ab2e2a59
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/FakeConfigBackendProvider.kt
@@ -0,0 +1,8 @@
+package net.thunderbird.core.configstore
+
+import net.thunderbird.core.configstore.backend.ConfigBackend
+import net.thunderbird.core.configstore.backend.ConfigBackendProvider
+
+internal class FakeConfigBackendProvider(private val backend: ConfigBackend) : ConfigBackendProvider {
+    override fun provide(id: ConfigId): ConfigBackend = backend
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfig.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfig.kt
new file mode 100644
index 00000000000..6d6ba20f570
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfig.kt
@@ -0,0 +1,6 @@
+package net.thunderbird.core.configstore
+
+internal data class TestConfig(
+    val stringValue: String,
+    val intValue: Int,
+)
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigDefinition.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigDefinition.kt
new file mode 100644
index 00000000000..d18649005fc
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigDefinition.kt
@@ -0,0 +1,18 @@
+package net.thunderbird.core.configstore
+
+internal class TestConfigDefinition(
+    override val mapper: TestConfigMapper,
+    override val defaultValue: TestConfig,
+    override val version: Int = 1,
+    override val migration: ConfigMigration = object : ConfigMigration {
+        override suspend fun migrate(currentVersion: Int, newVersion: Int, current: Config): ConfigMigrationResult {
+            return ConfigMigrationResult.NoOp
+        }
+    },
+) : ConfigDefinition<TestConfig> {
+    override val id: ConfigId = ConfigId("test_backend", "test_feature")
+    override val keys: List<ConfigKey<*>> = listOf(
+        mapper.stringKey,
+        mapper.intKey,
+    )
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigMapper.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigMapper.kt
new file mode 100644
index 00000000000..b9150f3e76a
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigMapper.kt
@@ -0,0 +1,23 @@
+package net.thunderbird.core.configstore
+
+internal class TestConfigMapper : ConfigMapper<TestConfig> {
+    val stringKey = ConfigKey.StringKey("string_key")
+    val intKey = ConfigKey.IntKey("int_key")
+
+    override fun toConfig(obj: TestConfig): Config {
+        return Config().apply {
+            this[stringKey] = obj.stringValue
+            this[intKey] = obj.intValue
+        }
+    }
+
+    override fun fromConfig(config: Config): TestConfig? {
+        val stringValue = config[stringKey] ?: return null
+        val intValue = config[intKey] ?: return null
+
+        return TestConfig(
+            stringValue = stringValue,
+            intValue = intValue,
+        )
+    }
+}
diff --git a/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigStore.kt b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigStore.kt
new file mode 100644
index 00000000000..25b28596e56
--- /dev/null
+++ b/core/configstore/api/src/commonTest/kotlin/net/thunderbird/core/configstore/TestConfigStore.kt
@@ -0,0 +1,8 @@
+package net.thunderbird.core.configstore
+
+import net.thunderbird.core.configstore.backend.ConfigBackendProvider
+
+internal class TestConfigStore(
+    provider: ConfigBackendProvider,
+    definition: ConfigDefinition<TestConfig>,
+) : BaseConfigStore<TestConfig>(provider, definition)
diff --git a/core/configstore/impl-backend/src/commonTest/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackendFactoryTest.kt b/core/configstore/impl-backend/src/commonTest/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackendFactoryTest.kt
new file mode 100644
index 00000000000..c03fbdaea5b
--- /dev/null
+++ b/core/configstore/impl-backend/src/commonTest/kotlin/net/thunderbird/core/configstore/backend/DataStoreConfigBackendFactoryTest.kt
@@ -0,0 +1,64 @@
+package net.thunderbird.core.configstore.backend
+
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isInstanceOf
+import java.io.File
+import kotlinx.coroutines.test.runTest
+import net.thunderbird.core.configstore.ConfigId
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+
+class DataStoreConfigBackendFactoryTest {
+
+    @Rule
+    @JvmField
+    val tempFolder = TemporaryFolder()
+
+    private lateinit var fileLocation: File
+
+    @Before
+    fun setUp() {
+        fileLocation = tempFolder.newFolder()
+    }
+
+    @Test
+    fun `create should return DefaultDataStoreConfigBackend`() {
+        // Arrange
+        val fileManager = FakeConfigBackendFileManager(fileLocation)
+        val testSubject = DataStoreConfigBackendFactory(fileManager)
+        val configId = ConfigId("test_backend", "test_feature")
+
+        // Act
+        val result = testSubject.create(configId)
+
+        // Assert
+        assertThat(result).isInstanceOf(DefaultDataStoreConfigBackend::class.java)
+    }
+
+    @Test
+    fun `create should use file manager to get file path`() = runTest {
+        // Arrange
+        val fileManager = FakeConfigBackendFileManager(fileLocation)
+        val testSubject = DataStoreConfigBackendFactory(fileManager)
+        val configId = ConfigId("test_backend", "test_feature")
+
+        // Act
+        val dataStore = testSubject.create(configId)
+        dataStore.clear()
+
+        // Assert
+        assertThat(fileManager.lastRequestedFileName).isEqualTo("test_backend.preferences_pb")
+    }
+
+    private class FakeConfigBackendFileManager(private val fileLocation: File) : ConfigBackendFileManager {
+        var lastRequestedFileName: String? = null
+
+        override fun getFilePath(backendFileName: String): String {
+            lastRequestedFileName = backendFileName
+            return fileLocation.resolve(backendFileName).absolutePath
+        }
+    }
+}
diff --git a/core/configstore/impl-backend/src/commonTest/kotlin/net/thunderbird/core/configstore/backend/DefaultDataStoreConfigBackendTest.kt b/core/configstore/impl-backend/src/commonTest/kotlin/net/thunderbird/core/configstore/backend/DefaultDataStoreConfigBackendTest.kt
new file mode 100644
index 00000000000..50c55ca17ed
--- /dev/null
+++ b/core/configstore/impl-backend/src/commonTest/kotlin/net/thunderbird/core/configstore/backend/DefaultDataStoreConfigBackendTest.kt
@@ -0,0 +1,216 @@
+package net.thunderbird.core.configstore.backend
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.doublePreferencesKey
+import androidx.datastore.preferences.core.emptyPreferences
+import androidx.datastore.preferences.core.floatPreferencesKey
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.longPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import assertk.assertThat
+import assertk.assertions.isEqualTo
+import assertk.assertions.isNull
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runTest
+import net.thunderbird.core.configstore.ConfigKey
+import org.junit.Before
+import org.junit.Test
+
+class DefaultDataStoreConfigBackendTest {
+
+    private lateinit var testSubject: DefaultDataStoreConfigBackend
+    private lateinit var fakeDataStore: FakeDataStore
+
+    @Before
+    fun setUp() {
+        fakeDataStore = FakeDataStore()
+        testSubject = DefaultDataStoreConfigBackend(fakeDataStore)
+    }
+
+    @Test
+    fun `read should return empty config when preferences are empty`() = runTest {
+        // Arrange
+        val keys = listOf(
+            ConfigKey.StringKey("string_key"),
+            ConfigKey.IntKey("int_key"),
+        )
+
+        // Act
+        val result = testSubject.read(keys).first()
+
+        // Assert
+        assertThat(result[keys[0]]).isNull()
+        assertThat(result[keys[1]]).isNull()
+    }
+
+    @Test
+    fun `read should return config with values from preferences`() = runTest {
+        // Arrange
+        val stringKey = ConfigKey.StringKey("string_key")
+        val intKey = ConfigKey.IntKey("int_key")
+        val booleanKey = ConfigKey.BooleanKey("boolean_key")
+        val longKey = ConfigKey.LongKey("long_key")
+        val floatKey = ConfigKey.FloatKey("float_key")
+        val doubleKey = ConfigKey.DoubleKey("double_key")
+
+        val keys = listOf(stringKey, intKey, booleanKey, longKey, floatKey, doubleKey)
+
+        fakeDataStore.updateData { preferences ->
+            preferences.toMutablePreferences().apply {
+                this[stringPreferencesKey("string_key")] = "string value"
+                this[intPreferencesKey("int_key")] = 123
+                this[booleanPreferencesKey("boolean_key")] = true
+                this[longPreferencesKey("long_key")] = 456L
+                this[floatPreferencesKey("float_key")] = 789.0f
+                this[doublePreferencesKey("double_key")] = 101.0
+            }
+        }
+
+        // Act
+        val result = testSubject.read(keys).first()
+
+        // Assert
+        assertThat(result[stringKey]).isEqualTo("string value")
+        assertThat(result[intKey]).isEqualTo(123)
+        assertThat(result[booleanKey]).isEqualTo(true)
+        assertThat(result[longKey]).isEqualTo(456L)
+        assertThat(result[floatKey]).isEqualTo(789.0f)
+        assertThat(result[doubleKey]).isEqualTo(101.0)
+    }
+
+    @Test
+    fun `update should store values in preferences`() = runTest {
+        // Arrange
+        val stringKey = ConfigKey.StringKey("string_key")
+        val intKey = ConfigKey.IntKey("int_key")
+        val keys = listOf(stringKey, intKey)
+
+        // Act
+        testSubject.update(keys) { config ->
+            config.apply {
+                this[stringKey] = "updated string"
+                this[intKey] = 456
+            }
+        }
+
+        // Assert
+        val preferences = fakeDataStore.data.first()
+        assertThat(preferences[stringPreferencesKey("string_key")]).isEqualTo("updated string")
+        assertThat(preferences[intPreferencesKey("int_key")]).isEqualTo(456)
+    }
+
+    @Test
+    fun `update with empty keys should not change preferences`() = runTest {
+        // Arrange
+        fakeDataStore.updateData { preferences ->
+            preferences.toMutablePreferences().apply {
+                this[stringPreferencesKey("string_key")] = "initial value"
+            }
+        }
+
+        // Act
+        testSubject.update(emptyList()) { config ->
+            config.apply {
+                this[ConfigKey.StringKey("string_key")] = "updated value"
+            }
+        }
+
+        // Assert
+        val preferences = fakeDataStore.data.first()
+        assertThat(preferences[stringPreferencesKey("string_key")]).isEqualTo("initial value")
+    }
+
+    @Test
+    fun `clear should remove all preferences`() = runTest {
+        // Arrange
+        fakeDataStore.updateData { preferences ->
+            preferences.toMutablePreferences().apply {
+                this[stringPreferencesKey("string_key")] = "string value"
+                this[intPreferencesKey("int_key")] = 123
+            }
+        }
+
+        // Act
+        testSubject.clear()
+
+        // Assert
+        val preferences = fakeDataStore.data.first()
+        assertThat(preferences.asMap().isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun `readVersion should return 0 when version key is not set`() = runTest {
+        // Act
+        val version = testSubject.readVersion("version_key")
+
+        // Assert
+        assertThat(version).isEqualTo(0)
+    }
+
+    @Test
+    fun `readVersion should return stored version`() = runTest {
+        // Arrange
+        fakeDataStore.updateData { preferences ->
+            preferences.toMutablePreferences().apply {
+                this[intPreferencesKey("version_key")] = 5
+            }
+        }
+
+        // Act
+        val version = testSubject.readVersion("version_key")
+
+        // Assert
+        assertThat(version).isEqualTo(5)
+    }
+
+    @Test
+    fun `writeVersion should store version in preferences`() = runTest {
+        // Act
+        testSubject.writeVersion("version_key", 10)
+
+        // Assert
+        val preferences = fakeDataStore.data.first()
+        assertThat(preferences[intPreferencesKey("version_key")]).isEqualTo(10)
+    }
+
+    @Test
+    fun `removeKeys should delete specified keys from preferences`() = runTest {
+        // Arrange
+        val stringKey = ConfigKey.StringKey("string_key")
+        val booleanKey = ConfigKey.BooleanKey("boolean_key")
+
+        fakeDataStore.updateData { preferences ->
+            preferences.toMutablePreferences().apply {
+                this[stringPreferencesKey("string_key")] = "string value"
+                this[intPreferencesKey("int_key")] = 123
+                this[booleanPreferencesKey("boolean_key")] = true
+            }
+        }
+
+        // Act
+        testSubject.removeKeys(setOf(stringKey, booleanKey))
+
+        // Assert
+        val preferences = fakeDataStore.data.first()
+        assertThat(preferences[stringPreferencesKey("string_key")]).isNull()
+        assertThat(preferences[booleanPreferencesKey("boolean_key")]).isNull()
+        assertThat(preferences[intPreferencesKey("int_key")]).isEqualTo(123)
+    }
+
+    private class FakeDataStore : DataStore<Preferences> {
+        private val emptyPreferences = emptyPreferences()
+        private val dataFlow = MutableStateFlow(emptyPreferences)
+
+        override val data: Flow<Preferences> = dataFlow
+
+        override suspend fun updateData(transform: suspend (t: Preferences) -> Preferences): Preferences {
+            val newData = transform(dataFlow.value)
+            dataFlow.value = newData
+            return newData
+        }
+    }
+}