Skip to content

KeyValueStore

KeyValueStore is the small storage primitive used across the persistence package. It speaks two value shapes — string and Uint8Array — and exposes a uniform Effect-based interface (get, set, has, remove, clear, …) over several interchangeable backends: an in-memory Map, the filesystem, a SQL table, and the browser’s Web Storage API. Every operation can fail with KeyValueStoreError.

When you need typed values rather than raw strings, wrap a store with toSchemaStore to get JSON encoding/decoding driven by a Schema. To share one backend across logical namespaces, use prefix.

import { KeyValueStore } from "effect/unstable/persistence";

Access the store with the KeyValueStore service tag inside Effect.gen, then call its operations. The simplest backend is layerMemory, a process-local Map.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
// Write a couple of string values
yield* store.set("user:1", "Alice");
yield* store.set("user:2", "Bob");
// Read one back — get resolves to `string | undefined`
const alice = yield* store.get("user:1");
// => "Alice"
// Membership and counting
const exists = yield* store.has("user:2");
// => true
const count = yield* store.size;
// => 2
// Remove a key, then confirm
yield* store.remove("user:1");
const gone = yield* store.get("user:1");
// => undefined
return { alice, exists, count, gone };
});
// Provide the in-memory backend and run
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

Raw stores only hold strings and bytes. To persist domain objects, wrap a store with toSchemaStore: values are JSON-encoded through the schema on set and decoded on get. get returns an Option (rather than undefined), and modify maps the decoded value in place.

import { Effect, Option, Schema } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const User = Schema.Struct({
name: Schema.String,
age: Schema.Number,
});
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
// Wrap the raw store with the User schema
const users = KeyValueStore.toSchemaStore(store, User);
// `set` takes the decoded Type and encodes it to JSON under the hood
yield* users.set("user:1", { name: "Alice", age: 30 });
// `get` decodes back into the typed value, wrapped in Option
const alice = yield* users.get("user:1");
// => Option.some({ name: "Alice", age: 30 })
const missing = yield* users.get("user:404");
// => Option.none()
// `modify` reads, applies `f`, and writes the result back
const updated = yield* users.modify("user:1", (u) => ({
...u,
age: u.age + 1,
}));
// => Option.some({ name: "Alice", age: 31 })
return { alice, missing, updated };
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

The typed operations widen both channels relative to the raw store:

  • get/set/modify can additionally fail with Schema.SchemaError.
  • get requires the schema’s DecodingServices, set requires its EncodingServices (and modify requires both). For plain schemas like the one above these are never, so nothing extra needs to be provided.

prefix returns a view of a store that prepends a fixed string to every key. This lets several logical stores safely share a single backend without colliding.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
// Two namespaced views over the same backing store
const users = KeyValueStore.prefix(store, "users:");
const sessions = KeyValueStore.prefix(store, "sessions:");
yield* users.set("1", "Alice"); // real key: "users:1"
yield* sessions.set("1", "tok_abc"); // real key: "sessions:1"
const user = yield* users.get("1");
// => "Alice"
const session = yield* sessions.get("1");
// => "tok_abc"
// The underlying store sees the full, prefixed keys
const total = yield* store.size;
// => 2
return { user, session, total };
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

The store interface. All operations return an Effect that may fail with KeyValueStoreError. get/set/modify/has are functions of a key; clear, size, and isEmpty are plain Effect values (not functions).

MemberSignatureDescription
get(key) => Effect<string | undefined, …>Read a string value, or undefined if absent.
getUint8Array(key) => Effect<Uint8Array | undefined, …>Read a value as bytes.
set(key, value: string | Uint8Array) => Effect<void, …>Write a string or binary value.
remove(key) => Effect<void, …>Delete a key.
clearEffect<void, …>Delete every entry.
sizeEffect<number, …>Count entries.
modify(key, f: (s: string) => string) => Effect<string | undefined, …>Read-map-write a string; undefined if the key was absent.
modifyUint8Array(key, f: (u: Uint8Array) => Uint8Array) => Effect<Uint8Array | undefined, …>Read-map-write bytes.
has(key) => Effect<boolean, …>Whether the key exists.
isEmptyEffect<boolean, …>Whether the store has no entries.
import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
const empty = yield* store.isEmpty;
// => true
yield* store.set("count", "1");
// modify reads the current string and writes the mapped result
const next = yield* store.modify("count", (s) => String(Number(s) + 1));
// => "2"
const present = yield* store.has("count");
// => true
const n = yield* store.size;
// => 1
const value = yield* store.get("count");
// => "2"
return { empty, next, present, n, value };
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

A Context.Service whose service type is the KeyValueStore interface. yield* the tag to access the store; provide an implementation with one of the layer* constructors below.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
// Access inside Effect.gen
const use = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
return yield* store.size;
});
// Provide a backend
Effect.runPromise(use.pipe(Effect.provide(KeyValueStore.layerMemory)));
// => 0

A Data.TaggedError (_tag: "KeyValueStoreError") carrying message and method, plus optional key and cause. Use Effect.catchTag to handle it.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
return yield* store.get("missing");
}).pipe(
Effect.catchTag("KeyValueStoreError", (error) =>
Effect.succeed(`failed in ${error.method}: ${error.message}`),
),
);
// error.method => e.g. "get"; error.key => the offending key (when applicable)
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

Builds a KeyValueStore from primitive callbacks. get, getUint8Array, set, remove, clear, and size are required; has, isEmpty, modify, and modifyUint8Array are derived for you (via get/size/set) unless you override them.

import { Effect, Encoding } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
// A minimal Map-backed store; has/isEmpty/modify are derived automatically.
const make = () => {
const map = new Map<string, string | Uint8Array>();
return KeyValueStore.make({
get: (key) =>
Effect.sync(() => {
const v = map.get(key);
return v === undefined || typeof v === "string"
? v
: Encoding.encodeBase64(v);
}),
getUint8Array: (key) =>
Effect.sync(() => {
const v = map.get(key);
return typeof v === "string" ? new TextEncoder().encode(v) : v;
}),
set: (key, value) => Effect.sync(() => void map.set(key, value)),
remove: (key) => Effect.sync(() => void map.delete(key)),
clear: Effect.sync(() => map.clear()),
size: Effect.sync(() => map.size),
});
};
const store = make();
Effect.runPromise(store.set("k", "v").pipe(Effect.andThen(store.has("k"))));
// => true

Adapts a backend that can only store strings into a full KeyValueStore. Binary values passed to set are encoded as base64; getUint8Array base64-decodes stored values and falls back to UTF-8 encoding for strings that are not valid base64. Used internally by layerStorage.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const store = KeyValueStore.makeStringOnly({
get: (key) => Effect.sync(() => globalThis.localStorage.getItem(key) ?? undefined),
set: (key, value) => Effect.sync(() => globalThis.localStorage.setItem(key, value)),
remove: (key) => Effect.sync(() => globalThis.localStorage.removeItem(key)),
clear: Effect.sync(() => globalThis.localStorage.clear()),
size: Effect.sync(() => globalThis.localStorage.length),
});
// store.set("k", new Uint8Array([1, 2, 3])) persists base64 ("AQID");
// store.getUint8Array("k") decodes it back => Uint8Array([1, 2, 3])

MakeOptions / MakeStringOptions / LayerSqlOptions

Section titled “MakeOptions / MakeStringOptions / LayerSqlOptions”

Option types for the constructors above and for layerSql. MakeOptions is Partial<KeyValueStore> with the six primitive operations made required; MakeStringOptions is the same but with a string-only set and no getUint8Array. LayerSqlOptions carries a single optional table (default "effect_key_value_store").

import type { KeyValueStore } from "effect/unstable/persistence";
// table defaults to "effect_key_value_store" when omitted
const opts: KeyValueStore.LayerSqlOptions = { table: "kv" };

Dual-signature combinator returning a view whose keys are all prefixed with the given string. Available data-first (prefix(store, "p:")) and data-last (prefix("p:")) for use in pipe.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
// data-last form, composed with pipe
const scoped = store.pipe(KeyValueStore.prefix("cache:"));
yield* scoped.set("a", "1"); // underlying key => "cache:a"
const raw = yield* store.get("cache:a");
// => "1"
return raw;
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

A Layer<KeyValueStore> backed by an in-process Map. No dependencies, no I/O errors, nothing persisted across restarts — ideal for tests and transient state.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
yield* store.set("k", "v");
return yield* store.get("k");
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));
// => "v"

A Layer<KeyValueStore, PlatformError, FileSystem | Path> that stores one file per key under directory. The directory is created (recursively) on layer construction, and keys are turned into file names with encodeURIComponent. Provide a platform FileSystem and Path (e.g. from @effect/platform-node).

import { Effect, Layer } from "effect";
import { NodeContext } from "@effect/platform-node";
import { KeyValueStore } from "effect/unstable/persistence";
// Files land in ./data, e.g. ./data/user%3A1 for the key "user:1"
const StoreLive = KeyValueStore.layerFileSystem("./data").pipe(
Layer.provide(NodeContext.layer),
);
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
yield* store.set("user:1", "Alice");
return yield* store.get("user:1");
// => "Alice"
});
Effect.runPromise(program.pipe(Effect.provide(StoreLive)));

A Layer<KeyValueStore, never, SqlClient> backed by a SQL table. On construction it issues CREATE TABLE IF NOT EXISTS (dialect-aware: SQLite/Postgres/MySQL/MSSQL) for the configured table, with columns id, value, and a value_type flag recording whether each value was stored as a string or as bytes. Supply a SqlClient from a driver layer.

import { Effect, Layer } from "effect";
import { SqliteClient } from "@effect/sql-sqlite-node";
import { KeyValueStore } from "effect/unstable/persistence";
// Driver layer supplies SqlClient; layerSql creates "kv" and reads/writes it.
const SqlLive = SqliteClient.layer({ filename: "kv.db" });
const StoreLive = KeyValueStore.layerSql({ table: "kv" }).pipe(
Layer.provide(SqlLive),
);
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
yield* store.set("user:1", "Alice");
return yield* store.get("user:1");
// => "Alice"
});
Effect.runPromise(program.pipe(Effect.provide(StoreLive)));

A Layer<KeyValueStore> backed by a Web Storage instance (localStorage or sessionStorage). It is string-only and routes through makeStringOnly, so binary values are persisted as base64. Pass a thunk that returns the storage object so evaluation is deferred to layer build time.

import { Effect } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
// In a browser environment:
const StoreLive = KeyValueStore.layerStorage(() => window.localStorage);
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
yield* store.set("theme", "dark");
return yield* store.get("theme");
// => "dark"
});
Effect.runPromise(program.pipe(Effect.provide(StoreLive)));

The typed view returned by toSchemaStore. It mirrors KeyValueStore but operates on decoded values: get returns Option<S["Type"]>, set/modify take the decoded type, and the encoding/ decoding operations widen the error channel with Schema.SchemaError and add the schema’s DecodingServices/EncodingServices to requirements. remove, clear, size, has, and isEmpty are unchanged from the underlying store.

import { Effect, Schema } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const Counter = Schema.Struct({ value: Schema.Number });
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
const counters: KeyValueStore.SchemaStore<typeof Counter> =
KeyValueStore.toSchemaStore(store, Counter);
yield* counters.set("hits", { value: 1 });
const has = yield* counters.has("hits");
// => true
const size = yield* counters.size;
// => 1
return { has, size };
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));

Wraps a KeyValueStore into a SchemaStore using the schema’s JSON codec. set encodes the value to a JSON string before writing (encode direction); get reads the string and decodes it back to the typed value wrapped in Option (decode direction).

import { Effect, Schema } from "effect";
import { KeyValueStore } from "effect/unstable/persistence";
const Point = Schema.Struct({ x: Schema.Number, y: Schema.Number });
const program = Effect.gen(function* () {
const store = yield* KeyValueStore.KeyValueStore;
const points = KeyValueStore.toSchemaStore(store, Point);
// Encode direction: the object is serialized to JSON and stored
yield* points.set("origin", { x: 0, y: 0 });
// raw string in the backend => '{"x":0,"y":0}'
// Decode direction: the JSON is parsed back into the typed Option
const origin = yield* points.get("origin");
// => Option.some({ x: 0, y: 0 })
const absent = yield* points.get("nope");
// => Option.none()
return { origin, absent };
});
Effect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));