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";Quickstart
Section titled “Quickstart”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 runEffect.runPromise(program.pipe(Effect.provide(KeyValueStore.layerMemory)));Storing typed values with SchemaStore
Section titled “Storing typed values with SchemaStore”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/modifycan additionally fail withSchema.SchemaError.getrequires the schema’sDecodingServices,setrequires itsEncodingServices(andmodifyrequires both). For plain schemas like the one above these arenever, so nothing extra needs to be provided.
Namespacing with prefix
Section titled “Namespacing with prefix”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)));Choosing a backend
Section titled “Choosing a backend”Reference
Section titled “Reference”KeyValueStore (interface)
Section titled “KeyValueStore (interface)”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).
| Member | Signature | Description |
|---|---|---|
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. |
clear | Effect<void, …> | Delete every entry. |
size | Effect<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. |
isEmpty | Effect<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)));KeyValueStore (service tag)
Section titled “KeyValueStore (service tag)”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.genconst use = Effect.gen(function* () { const store = yield* KeyValueStore.KeyValueStore; return yield* store.size;});
// Provide a backendEffect.runPromise(use.pipe(Effect.provide(KeyValueStore.layerMemory)));// => 0KeyValueStoreError
Section titled “KeyValueStoreError”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"))));// => truemakeStringOnly
Section titled “makeStringOnly”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 omittedconst opts: KeyValueStore.LayerSqlOptions = { table: "kv" };prefix
Section titled “prefix”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)));layerMemory
Section titled “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"layerFileSystem
Section titled “layerFileSystem”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)));layerSql
Section titled “layerSql”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)));layerStorage
Section titled “layerStorage”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)));SchemaStore (interface)
Section titled “SchemaStore (interface)”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)));toSchemaStore
Section titled “toSchemaStore”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)));