slap_kv

slap_kv provides a partitioned key-value store that persists data using SlateDB, built on slap_cluster and slap_slatedb. Rows are addressed by a partition and a key; a partition's rows are on one shard, in key order. Writes are acknowledged once durable, reads return only durable rows, and writes can be conditional on a row's version.

Use slap_kv when you need records grouped by a partition, such as a user's settings or a document's metadata. It routes each partition to a shard, lets you list its keys in order, and uses versions to detect concurrent updates. This saves you from building shard ownership and conditional writes on top of slap_slatedb. If you need direct SlateDB transactions or a different key layout, use slap_slatedb instead. slap_kv has no cross-partition scan or transaction.

slap_kv is meant to be embedded: your application starts Slap.KV.Cluster, calls Slap.KV in-process, and can mount Slap.KV.HTTP.Router in its Plug pipeline. The slap package runs slap_kv as a standalone server (mix slap.server --kv --store memory).

Installation

Add slap_kv to the dependencies in your application's mix.exs:

defp deps do
[
{:slap_kv, "~> 0.1.0"}
]
end

Run mix deps.get. API documentation is on HexDocs.

Example

In an application that depends on slap_kv, run this in iex -S mix with a fresh local directory:

iex> {:ok, _} = Slap.KV.Cluster.start_link(store: {:local, "/tmp/slap-kv"}, path: "kv", shards: 8); :ok
:ok
iex> {:ok, v1} = Slap.KV.put("user:42", "profile", "Ada", if_version: :absent); :ok
:ok
iex> {:ok, %{value: value, version: ^v1}} = Slap.KV.get("user:42", "profile"); value
"Ada"
iex> {:error, {:conflict, ^v1}} = Slap.KV.put("user:42", "profile", "other", if_version: :absent); :conflict
:conflict
iex> {:ok, v2} = Slap.KV.put("user:42", "profile", "Ada Lovelace", if_version: v1); :ok
:ok
iex> {:ok, %{rows: rows, cursor: nil}} = Slap.KV.scan("user:42", prefix: "pro"); rows
[{"profile", "Ada Lovelace"}]
iex> :ok = Slap.KV.delete("user:42", "profile", if_version: v2)
:ok

An application can define its own cluster module and pass it to KV calls and the HTTP router:

defmodule MyApp.KVCluster do
use Slap.KV.Cluster, otp_app: :my_app
end
children = [{MyApp.KVCluster, store: {:local, "/tmp/slap-kv"}, shards: 8}]
{:ok, _} = Slap.KV.put("users", "42", "Ada", cluster: MyApp.KVCluster)

Supervise one KV cluster per VM. A Streams cluster can run on the same VM. Run one Slap.Cluster.Peers per VM; its connections to other nodes serve every cluster on that VM.

Semantics

How it works

HTTP

Slap.KV.HTTP.Router serves KV over HTTP, under /v1/kv by default. To try it, run the standalone server from slap in another terminal:

mix slap.server --kv --store memory --kv-shards 4

The memory store loses its data when the server stops. Then:

curl -X PUT --data-binary 'hello' -H 'If-None-Match: *' -i http://localhost:4437/v1/kv/p/greeting
curl -i http://localhost:4437/v1/kv/p/greeting
curl 'http://localhost:4437/v1/kv/p?prefix=gr&limit=10'

The PUT returns 204 and an ETag containing the new version; the GET returns 200 with the value and the same ETag. To replace or delete the row, send that value in If-Match. A stale version returns 412 with the current ETag.

Partitions and keys are percent-encoded path segments. In scan responses, keys, values, and the cursor are unpadded base64url. A 503 response (with Retry-After) means that the shard was unavailable or the request timed out; a write may still have been applied. The router does not authenticate requests.

Storage

A row's SlateDB key is the partition's length (16 bits), the partition, then the key, so a partition is one contiguous key range and scans never cross into another partition. Keys may be up to SlateDB's limit of 65,535 bytes once encoded. Deletes are SlateDB deletes (tombstones, removed by compaction).

Limits

Benchmarks

bench/partition_writers.exs measures throughput, latency, and writer mailbox length with unconditional, warm conditional, and cold conditional writes. A cold conditional write waits for a store read and blocks its partition writer; increasing partition_writers spreads that delay across partitions. Measure on the intended store before changing the default of 16.

Tests

mix test # SLAP_KV_PROP_RUNS=1000 for more model sequences

The model test covers conditional writes, scans, writer replacement, and cluster restarts. test/durability_test.exs checks durable replies, pending writes, and fencing. Set SLAP_KV_PROP_RUNS to run more model sequences.

License

Slap is released under the terms of the Apache License 2.0.

Copyright (c) 2026, Michael Russo.