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.