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.