Capri

Type-safe atomic Gleam bindings for Khepri.

Getting Started

Initialize Khepri and create a StoreHandle with lifecycle.bind/2. You will use this handle for accessing and manipulating your Khepri store/cluster.

import capri/lifecycle
import gleam/erlang/atom
let assert Ok(store) =
lifecycle.bind(atom.create("my_app"), "./data")

Then follow the rest of this README to get an overview of how to utilize the library.

Repository

Repositories represent the persistence layer for terms, as defined in traditional Domain-Driven Design.

Tip

This module doesn't use the Active Record pattern, since objects don't exist in Gleam, rendering it impossible to create "active" Records.

Usage

Use repository.bind/3 to open or create a repository for a given store:

let assert Ok(users) =
repository.new("users", 1, user_decoder, [])
let users_path = path.from_key(path.String("users"))
let assert Ok(prefix) = repository.bind(store, users_path, users)
let user_path = repository.child(prefix, path.String("user-123"))
let assert Ok(Nil) = repository.put(store, user_path, user)

Available operations can be found at the package API docs.

Migrations

Repository migrations are atomic, reversible, and versioned transformations over values & structure of repositories.

Warning

While 'unsafe' migrations (migrations that are not reversible) can be made, you are strongly advised to avoid them.

Usage

let assert Ok(one_to_two) =
repository.migration(
1,
2,
fn(entries) {
// Transform the complete keyed repository dataset.
list.map(entries, v1_to_v2)
},
fn(entries) {
list.map(entries, v2_to_v1)
},
)
let assert Ok(users) =
repository.new(
"users",
2,
user_decoder,
[one_to_two], // migration chain
)
let assert Ok(Nil) =
repository.migrate_all(store, users_path, users) // run migration chain

Projections

Projections are typed, derived views maintained by Khepri in local ETS tables. They are ephemeral caches, not authoritative repository state.

Usage

import capri/projection
let assert Ok(by_email) =
users
|> projection.new(fn(user) { user.email })
|> projection.named(atom.create("users_by_email"))
let assert Ok(Nil) =
projection.register(store, prefix, by_email)
let assert Ok(user) =
projection.get(by_email, "reese@example.com")

Use projection.many for one-to-many lookups:

let assert Ok(by_team) =
users
|> projection.new(fn(user) { user.team_id })
|> projection.many
|> projection.named(atom.create("users_by_team"))
let assert Ok(Nil) =
projection.register(store, prefix, by_team)
let assert Ok(team) =
projection.all(by_team, team_id)

Projection keys retain their Gleam type. Projection names are bounded atoms and should not be created from unbounded or user-controlled input.

Use projection.unregister to remove a projection and projection.rebuild to rebuild it from the repository.

Lifecycle

Use lifecycle.bind/2 to initialize Khepri, and get a StoreHandle. To connect to a cluster, use lifecycle.reset_and_join_cluster/2.

Warning

lifecycle.reset_and_join_cluster/2 and lifecycle.reset_local_member/1 erase local data.

Naming

Khepri sounds phonetically similar to capri, and capri, capris being a type of pants, is like "pants" for Khepri :P.

Capri: Type-safe atomic Gleam bindings for Khepri. Copyright (C) 2026 Software Freedom Conservancy, et. al.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.