KStore: typed key value storage for Kotlin multiplatform
A tiny Kotlin multiplatform library that assists in saving and restoring objects to and from disk using kotlinx.coroutines, kotlinx.serialisation and kotlinx.io
At a glance
- What is it?
- KStore is a small Kotlin multiplatform library that saves and restores serializable objects to disk through kotlinx.serialization, kotlinx.coroutines and kotlinx.io, with in-memory caching, default values, read-write locking and a migration path. Three artifacts cover the common module, file based storage and browser storage.
- Who is it for?
- Adopt KStore if you are building a Kotlin multiplatform application that needs to persist small serializable models on Android, iOS, desktop, Node and the browser from one shared API, and you want the storage details handled for you. Do not adopt it if you need a large database, concurrent transactional writes or a schema that evolves without migrations, since the library is scoped to a key value store with an explicit migration step.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 18 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A key value store, not a database
KStore is described as a tiny Kotlin multiplatform library that assists in saving and restoring objects to and from disk using kotlinx.coroutines, kotlinx.serialization and kotlinx.io. It is inspired by RxStore, which is the closest thing to a lineage for the design.
The scope is worth being clear about, because the word store can suggest more than it delivers here. This is a key value store. You give it a serializable model and a location, and it reads and writes that model as a single value. The features list spells out what that buys you: read-write locks with a mutex FIFO lock, in-memory caching where a value is read once from disk and reused, default values so a missing file is not a problem, migration support for moving your data, and multiplatform coverage.
The type is in the API. A store is a KStore of your model, so the compiler carries the type through get, set and update. The at-a-glance example is short enough to read as the whole surface:
import kotlin.io.path.Path
import kotlinx.serialization.Serializable
// Take any serializable model
@Serializable data class Pet(val name: String, val age: Int)
// Create a store
val store: KStore<Pet> = storeOf(file = Path("path/to/my_cats.json"))
// Get, set, update or delete values
val mylo: Pet? = store.get()
store.set(mylo)
store.update { pet: Pet? ->
pet?.copy(age = pet.age + 1)
}
store.delete()
// Observe for updates
val pets: Flow<Pet?> = store.updatesThe read, write, update and delete operations, plus an updates flow, is a small but complete API. The update function takes a lambda over the current value, so a read modify write cycle is expressed as one operation rather than three, and the flow lets a screen react to a change written by another part of the app.
Three artifacts, and the choice is about the target platform
The dependency setup shows the packaging clearly, and it is a two step decision rather than one.
The first dependency is the common module, which is the part every target shares:
# for common
kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
# for android, iOS, desktop and nodeJs
kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
# for jsBrowser and wasmJsBrowser
kstore-storage = { module = "io.github.xxfast:kstore-storage", version.ref = "kstore" }So the split is: kstore for the API, kstore-file for Android, iOS, desktop and Node, and kstore-storage for the browser targets, both jsBrowser and wasmJsBrowser. The readme also notes that depending on your target platforms you need to add platform configurations, with the details on the installation page in the documentation.
The repository layout matches the packaging, with a kstore directory for the common module, a kstore-file directory, a kstore-storage directory, and a kotlin-js-store directory alongside them, plus a sample directory and a docs directory. The settings.gradle.kts ties the modules together.
The build is a Gradle project with build.gradle.kts at the root and a gradle wrapper, and the CI badge points at a build workflow with codecov coverage on master. The library targets Kotlin 2.4.0 and carries the Kotlin Beta stability badge, which is a signal worth reading carefully in a multiplatform library, since the multiplatform targets are where APIs move most often.
The published coordinates are io.github.xxfast on Maven Central, and the version is taken from a version.ref called kstore, so a single version property drives all three modules.
Why caching, locking and default values exist
The three behavioural features are what separate this from writing a file yourself, and each solves a specific problem.
In-memory caching means a value is read once from disk and reused. If your app reads a settings object on every screen, a file-backed store without caching turns every read into a disk operation and a deserialization. The cache means the read happens on first access and the value is held in memory afterwards.
Read-write locks with a mutex FIFO lock are the concurrency story. A mutex that grants access in first-in first-out order is a specific choice: a plain unfair mutex can starve a waiting reader indefinitely under contention, whereas a FIFO lock makes the wait bounded by the queue. For a store holding a small model, that predictability is worth having, because the alternative is a lock that occasionally never releases to a particular caller.
Default values solve the cold start case, and the feature list phrases it as no file, no problem. A store that has never been written should not throw when read. Returning a default means your code can call get on a fresh install without checking whether the file exists first, and the value can still be written later through the normal set path.
The trade these features imply is that the in-memory copy is authoritative for reads once it has been populated. If two processes or two application instances write the same file, the cache is not shared between them, and the read-write lock protects access within one instance rather than across instances. That is a reasonable scope for a per-application settings store and a poor fit for a shared file.
Migration support means the stored shape can change
The migration feature is described in the readme as moving shop, take your data with you, which is the informal way of saying that the on-disk format is allowed to evolve.
Serialized models are versioned by the shape of the data class, and a data class that gains a field or renames one changes what is written. For a store this is a real problem, because the file on disk from the previous version of the app has to be readable by the new version, and a user who skips several releases has a file from an older shape again.
The migration support is what sits between those two facts. A migration step is code you write, so it is explicit and reviewable, and the readme lists it as a first class feature rather than an afterthought. That is the right design for a library, because the alternative, silently ignoring data it cannot parse, loses user data in a way that is hard to diagnose from a bug report.
The repository does not show the migration API in the at-a-glance example, so the exact shape of it lives in the API reference on the documentation site. What can be said from the readme is that migration is part of the feature set and is intended to be used when you change the model.
The same caution applies to the serialization format itself. kotlinx.serialization is what writes the data, and the model has to be marked @Serializable for the store to accept it, which is a compile-time requirement rather than a runtime failure. The example makes that explicit with the @Serializable annotation on the Pet data class.
Where this sits against doing it yourself
The alternative is not a competing library so much as three lines of your own code, and the honest comparison matters because KStore is small.
Writing a store by hand means reading a file, deserializing it with kotlinx.serialization, and writing it back. That is genuinely small, and if you only target one platform and never change the model, it may be the better answer. There is no dependency, no multiplatform expectation, and no behaviour to learn.
What you take on is the parts KStore handles. The cache is the first, since a naive implementation reads the file every time. The locking is the second, since concurrent access to a file you are rewriting is exactly where the naive version breaks, and it breaks under load in a way that is hard to reproduce. The defaults are the third. And the migration path is the fourth, which is the one that costs the most to reinvent badly.
The multiplatform angle is the other reason to take the library rather than the code. The three modules exist precisely because file access on a phone, on a desktop, in Node and in a browser are four different problems, and the readme shows a single storeOf call in the example. Writing that yourself means writing four implementations, and the browser one is not a file at all, which is why there is a separate storage artifact rather than a file artifact for the browser.
The limitation of the library is that it is scoped to a single value per store. If your application needs indexes, queries across many records, or transactional writes, you are past what this design covers, and the answer is a real database rather than a better key value store.
Editorial conclusion
Adopt KStore if you are building a Kotlin multiplatform application that needs to persist small serializable models on Android, iOS, desktop, Node and the browser from one shared API, and you want the storage details handled for you. Do not adopt it if you need a large database, concurrent transactional writes or a schema that evolves without migrations, since the library is scoped to a key value store with an explicit migration step. Before you commit, add the module matching your target platforms, confirm your model is marked with @Serializable, and decide which of the three modules you need so you do not pull browser storage into a mobile build.
Frequently asked questions
What is KStore?
KStore is a tiny Kotlin multiplatform library that helps you save and restore objects to and from disk using kotlinx.coroutines, kotlinx.serialization and kotlinx.io. It provides read-write locks with a FIFO mutex, in-memory caching, default values, migration support and multiplatform targets.
How do I add KStore to a Kotlin project?
KStore is published on Maven Central. Add the common kstore module for the API, the kstore-file module for Android, iOS, desktop and Node, or the kstore-storage module for jsBrowser and wasmJsBrowser, and add the platform configurations for your targets as the installation page describes.
What is the difference between kstore-file and kstore-storage?
kstore-file is the file based implementation for Android, iOS, desktop and Node, while kstore-storage is for the browser targets jsBrowser and wasmJsBrowser. The common kstore module holds the API that both implement.
How do I read, write and observe values in KStore?
Create a store with storeOf, then use get, set, update and delete on it, and observe changes through the updates flow. The update function takes a lambda over the current value, so a read modify write cycle is a single operation.
Is KStore suitable for large or relational data?
No. KStore is a key value store for a single serializable value per store, with caching, locking and an explicit migration path. If you need indexes, queries across records or transactional writes, you want a real database rather than this library.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/xxfast-kstore)Was this useful?