Session & User State

The signed-in user, an auth token, the active profile — state that's scoped to a login session rather than the app's durable settings. Keeping it in its own collection keeps it separate from data that should survive a logout.

@Serializable
data class Session(
    val userId: String,
    val displayName: String,
    val authToken: String,
)

val sessionStore = Documents.collection("session")
val session = sessionStore.document<Session>("current")

// sign in
session.set(Session(userId = "u_42", displayName = "Mara", authToken = "…"))

// react to sign-in / sign-out anywhere in the UI
session.flow().collect { current ->
    isSignedIn = current != null
}

Logging out deletes the session document — there is no separate "clear the whole collection" call in the public API, so sign-out is an explicit delete() on each document that should end with the session:

fun signOut() {
    session.delete()
}

Encrypting the auth token

authToken is a realistic candidate for field-level encryption — the session collection is a natural encryption boundary. FieldDecorator is a complete extension point for this; the example below uses cryptography-kotlin (dev.whyoleg.cryptography, v0.6.0), one library choice among several, and a dependency your app adds, not one Documents pulls in:

// build.gradle.kts (your app/module, not Documents itself)
implementation("dev.whyoleg.cryptography:cryptography-core:0.6.0")
implementation("dev.whyoleg.cryptography:cryptography-provider-optimal:0.6.0")
import dev.whyoleg.cryptography.CryptographyProvider
import dev.whyoleg.cryptography.algorithms.AES

class AesGcmFieldDecorator(
    private val key: AES.GCM.Key,
) : FieldDecorator {

    private val cipher = key.cipher()

    override fun wrap(fieldName: String, bytes: ByteArray): ByteArray =
        cipher.encryptBlocking(
            plaintext = bytes,
            associatedData = fieldName.encodeToByteArray(),
        )

    override fun unwrap(fieldName: String, bytes: ByteArray): ByteArray =
        cipher.decryptBlocking(
            ciphertext = bytes,
            associatedData = fieldName.encodeToByteArray(),
        )
}

val provider = CryptographyProvider.Default
val key = provider.get(AES.GCM).keyGenerator().generateKeyBlocking()

val sessionStore = Documents.collection("session") {
    decorators = listOf(AesGcmFieldDecorator(key))
}
val session = sessionStore.document<Session>("current")

Note the Blocking suffix — encryptBlocking/decryptBlocking, not the suspending encrypt/decrypt — since FieldDecorator.wrap and unwrap are synchronous. encryptBlocking bundles the nonce and auth tag into the returned ByteArray, so AES-GCM's authentication catches a tampered or corrupted stored value instead of decoding it into garbage, and passing fieldName as associated data means a ciphertext copied from one field's key into another's fails to decrypt rather than silently succeeding with the wrong data. Key generation, storage, and rotation are your app's own responsibility — see the cryptography-kotlin docs for that part, and Decorations / Field Decorators for how the extension point itself works.

See Opening Documents for when to reach for a named collection versus the default store, and keep in mind the single-process constraint that applies to every store, including this one.