Skip to content

Key Operations

All key operations are performed through a bucket handle. Get a handle by calling kv.bucket("name") with the name of an existing bucket.

Writing Values

Use put to unconditionally write a value. If the key already exists, its value is overwritten. The call returns the new revision number.

import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });
const kv = client.kv();
const bucket = kv.bucket("my-bucket");
const { revision } = await bucket.put("user:123", { name: "Alice", role: "admin" });
console.log("Revision:", revision); // 1

Reading Values

Use get to read a value by key. The returned entry includes the key, value, revision number, creation timestamp, and the last operation type.

Values are stored as raw bytes on the server. The SDK returns entry.value as unknown — the wire encoding is base64 for binary content and a JSON string for JSON content. The SDK does not auto-decode; if you stored JSON, decode it on read — in Node JSON.parse(Buffer.from(entry.value as string, "base64").toString("utf-8")), in the browser JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(entry.value as string), (c) => c.charCodeAt(0)))) (plain atob corrupts multi-byte UTF-8) — or store as a string explicitly.

const entry = await bucket.get("user:123");
console.log(entry.key); // "user:123"
console.log(entry.value); // base64 string of stored bytes (decode + JSON.parse to recover an object)
console.log(entry.revision); // 1
console.log(entry.created_at); // "2026-02-22T10:00:00Z"
console.log(entry.operation); // "put"

Deleting Values

Soft Delete

Use delete to place a tombstone marker on the key. The key no longer appears in reads or listings, but its history is preserved according to the bucket’s history setting.

await bucket.delete("user:123");
// Key is tombstoned — get() now throws IronflowError with code "HTTP_404"

Hard Delete (Purge)

Use purge to permanently remove a key and all of its revision history. This is irreversible.

await bucket.purge("user:123");
// Key and all history permanently removed

Atomic Operations

Ironflow supports revision-based concurrency control for safe concurrent writes. Every write to a key increments its revision number. Atomic operations use this revision to detect conflicts and prevent lost updates.

Create (If-Not-Exists)

Use create to write a value only if the key does not already exist. If the key exists, the operation fails with HTTP 412. This is useful for safe initialization of keys that should only be set once.

try {
const { revision } = await bucket.create("config:feature-flags", {
darkMode: true,
betaAccess: false,
});
console.log("Created at revision:", revision);
} catch (err) {
// Key already exists (HTTP 412)
console.error("Key already exists:", err.message);
}

Update (Compare-and-Swap)

Use update to write a value only if the key’s current revision matches the one you provide. This prevents lost updates when multiple clients modify the same key concurrently. The typical pattern is read-modify-write:

  1. Read the current entry to get the latest revision.
  2. Modify the value locally.
  3. Update with the revision from step 1. If another client wrote in between, the revision will have changed and the update fails with HTTP 412.
// 1. Read the current value
const entry = await bucket.get("user:123");
const current = JSON.parse(Buffer.from(entry.value as string, "base64").toString("utf-8"));
// 2. Modify locally
const updated = { ...current, role: "superadmin" };
// 3. Write back with the revision guard
try {
const { revision } = await bucket.update("user:123", updated, entry.revision);
console.log("Updated to revision:", revision);
} catch (err) {
// Revision mismatch (HTTP 412) — another client wrote first
console.error("Conflict — retry with fresh data:", err.message);
}

Listing Keys

Use listKeys to retrieve all keys in a bucket, optionally filtered by a wildcard pattern.

Wildcard filters follow NATS subject-matching rules, where . is the token separator. A wildcard like user.* matches keys structured as user.123, not user:123 — the latter is a single token with no separator. If you need prefix-style filtering, use . as the separator in your key names.

// List all keys
const allKeys = await bucket.listKeys();
console.log(allKeys); // ["user.123", "user.456", "config.feature-flags"]
// Filter with a wildcard pattern (keys must use "." separators for this to match)
const userKeys = await bucket.listKeys("user.*");
console.log(userKeys); // ["user.123", "user.456"]

Wildcard patterns follow NATS subject-matching rules. . is the token separator:

Pattern Matches Does not match
* Any single-token key (user, flag) Multi-token keys (user.123)
user.* user.123, user.456 user.123.name, user:123
> One or more tokens (matches everything)
user.> user.123, user.123.name, user.123.name.first config.flags, user:123

Watching for Changes

Watch provides real-time notifications when keys are created, updated, or deleted. Changes are delivered over WebSocket and are available in the browser SDK.

import { ironflow } from "@ironflow/browser";
const bucket = ironflow.kv().bucket("my-bucket");
// Watch all keys matching "user.*"
const watcher = bucket.watch({
onUpdate: (event) => {
console.log(`${event.key} ${event.operation}: rev ${event.revision}`);
console.log("New value:", event.value);
console.log("Bucket:", event.bucket);
},
onError: (err) => console.error(err),
onClose: () => console.log("Watch ended"),
}, { key: "user.*" });
// Later: stop watching
watcher.stop();

Each KVWatchEvent contains:

Field Type Description
type "kv_update" Always "kv_update"
key string The key that changed
value string The new value as base64-encoded bytes (empty for deletes)
revision number New revision number
operation "put" | "delete" What happened
bucket string Bucket name

event.value is the raw stored bytes encoded as base64 — not a parsed JSON object. If you stored JSON, decode it before use — in Node JSON.parse(Buffer.from(event.value as string, "base64").toString("utf-8")), in the browser via atob + TextDecoder (plain atob corrupts multi-byte UTF-8).

Error Handling

KV operations return standard HTTP error codes for common failure scenarios:

Error HTTP Status When
Key not found 404 Get or delete on a missing key
Key already exists 412 Create when the key already exists
Revision mismatch 412 Update with a stale revision
Bucket not found 404 Operations on a bucket that does not exist
import { IronflowError, UnauthenticatedError, UnauthorizedError } from "@ironflow/node";
try {
await bucket.create("user:123", { name: "Alice" });
} catch (err) {
if (err instanceof UnauthenticatedError) {
console.error("Invalid or missing API key");
} else if (err instanceof UnauthorizedError) {
console.error("Insufficient permissions for this operation");
} else if (err instanceof IronflowError) {
// IronflowError includes the HTTP status in the message
if (err.message.includes("412")) {
console.error("Key already exists — use put() to overwrite or update() with a revision");
} else if (err.message.includes("404")) {
console.error("Bucket not found — create it first");
} else {
console.error("Ironflow error:", err.message);
}
} else {
console.error("Unexpected error:", err);
}
}

Use the global onError handler on createClient() to observe all KV errors centrally without wrapping each call in try/catch.