Getting Started
Vault is the shared persistence foundation underneath Karango and Monko. This page walks through the core concepts you need before choosing a backend.
1. Define your entity
Annotate a data class with @Vault. Your domain model stays free of database concerns.
import io.peekandpoke.ultra.vault.Vault
@Vault
data class Article(
val title: String,
val body: String,
val tags: List<String> = emptyList(),
) 2. The Storable wrappers
Vault wraps every entity in one of three types, depending on its lifecycle stage. Database metadata lives in the wrapper, never in your data class.
import io.peekandpoke.ultra.vault.New
import io.peekandpoke.ultra.vault.Stored
import io.peekandpoke.ultra.vault.Ref
// Before saving — no database metadata yet
val new: New<Article> = New(Article("Draft", "..."))
// After saving — carries _id, _key, _rev from the database
val stored: Stored<Article> = repo.insert(Article("Hello", "World"))
stored._id // "articles/abc123"
stored._key // "abc123"
stored() // Article(title="Hello", body="World")
// A lazy reference — resolves on first access
val ref: Ref<Article> = stored.asRef 3. Working with Stored
Use modify to create an updated copy that preserves database identity,
or transform to change the value type entirely.
// Access the value
val article: Article = stored()
// Create a modified copy — same _id, _key, _rev
val updated: Stored<Article> = stored.modify { it.copy(title = "New Title") }
// Transform to a different type
val titleOnly: Stored<String> = stored.transform { it.title }
// Replace the value outright
val replaced: Stored<Article> = stored.withValue(Article("Replaced", "Content")) 4. Resolving values
suspend fun resolve() and its shorthand suspend operator fun invoke() work
uniformly on all Storable types. For Stored and New, resolution is instant.
For Ref, it may suspend to load from the database.
import io.peekandpoke.ultra.vault.Storable
suspend fun printTitle(storable: Storable<Article>) {
// Both forms are equivalent:
val a1: Article = storable.resolve()
val a2: Article = storable() // shorthand
println(a1.title)
}
// Works the same for any wrapper type
printTitle(new) // instant
printTitle(stored) // instant
printTitle(ref) // may suspend to load from DB, then cached 5. Cursors
Cursor<T> wraps a Kotlin Flow and provides suspend-friendly collection operations.
Repository queries return cursors, so you can stream results lazily or collect them all at once.
import io.peekandpoke.ultra.vault.Cursor
import io.peekandpoke.ultra.vault.filter
import io.peekandpoke.ultra.vault.map
import io.peekandpoke.ultra.vault.firstOrNull
val cursor: Cursor<Stored<Article>> = repo.findAll()
// Collect everything
val all: List<Stored<Article>> = cursor.toList()
// Transform
val titles: List<String> = cursor.map { it().title }
// Filter
val tagged: List<Stored<Article>> = cursor.filter { "kotlin" in it().tags }
// First match
val first: Stored<Article>? = cursor.firstOrNull { it().title == "Hello" } 6. Choose your backend
Vault defines the abstractions. To actually persist data, pick a backend and follow its getting-started guide.