Current section
Files
Jump to
Current section
Files
README.md
# 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.
```gleam
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.
> [!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 `capri.repository/3` to define a repository, then use `capri.bind/3` to open or create it for a given store:
```gleam
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.
> [!WARNING]
> While unsafe (irreversible) migrations can be created, you are strongly advised to avoid them.
```gleam
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:
```gleam
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.
```gleam
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`.
> [!WARNING]
> `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/>.