Capri

Type-safe, atomic Gleam bindings for Khepri.

See the generated module documentation for the complete API reference. This README focuses on the core concepts and safety model.

Getting Started

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

import capri
import gleam/erlang/atom

let assert Ok(store) =
  capri.init(atom.create("my_app"), "./data")

Repositories

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

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 capri.repository/3 to define a repository, then use capri.bind/3 to open or create it for a given store:

import capri
import capri/path
import capri/types
import gleam/dynamic/decode
import gleam/option.{Some}

let assert Ok(users) =
  capri.repository(
    "users",
    1,
    types.Decoder(user_decoder),
  )

let users_path =
  path.from_key(types.String("users"))

let assert Ok(users_prefix) =
  capri.bind(store, users_path, users)

let user_path =
  capri.child(users_prefix, types.String("user-123"))

let assert Ok(Nil) =
  capri.put(store, user_path, user)

let assert Ok(Some(stored_user)) =
  capri.get(store, user_path)

Binding validates that the repository identity and version match the metadata stored at the prefix. The resulting typed prefix and child paths carry the repository capability used to decode and validate records.

Migrations

Migrations are atomic, versioned, and reversible transformations of data & schema.

Each migration includes the decoder for its target version, so schemas change in lockstep. Every step must increment the repository version by exactly one.

While unsafe (irreversible) migrations can be created, you are strongly advised to avoid them.

import capri
import capri/types

let assert Ok(users_v1) =
  capri.repository(
    "users",
    1,
    types.Decoder(user_v1_decoder),
  )

let assert Ok(users_v2) =
  capri.add_migration(
    users_v1,
    2,
    types.Decoder(user_v2_decoder),
    fn(entry) {
      let types.Entry(key, user_v1) = entry
      Ok(types.Entry(key, migrate_user_to_v2(user_v1)))
    },
    fn(entry) {
      let types.Entry(key, user_v2) = entry
      Ok(types.Entry(key, restore_user_v1(user_v2)))
    },
  )

let assert Ok(Nil) =
  capri.migrate_to_current(store, users_path, users_v2)

Projections

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

Set projections

The default projection type. Use projection.get/2 to fetch a sole value from a set projection:

import capri/projection
import gleam/erlang/atom
import gleam/option.{Some}

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, users_prefix, by_email)

let assert Ok(Some(user)) =
  projection.get(by_email, "capri@example.com")

Bag projections

Use projection.many/1 to create a bag projection that allows multiple records to share a key. Use projection.all/2 to query for values.

import capri/projection
import gleam/erlang/atom

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, users_prefix, by_team)

let assert Ok(team_members) =
  projection.all(by_team, team_id)

Clustering

To join a cluster, use capri.reset_and_join_cluster/2.

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

Naming

Khepri sounds phonetically similar to Capri. Capris are a type of pants, making Capri “pants” for Khepri.

Copyright

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/.

Search Document