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, parse it on read (JSON.parse(atob(entry.value as string))) 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");
// 2. Modify locally
const updated = { ...entry.value, 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:

PatternMatchesDoes not match
*Any single-token key (user, flag)Multi-token keys (user.123)
user.*user.123, user.456user.123.name, user:123
>One or more tokens (matches everything)
user.>user.123, user.123.name, user.123.name.firstconfig.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:

FieldTypeDescription
type"kv_update"Always "kv_update"
keystringThe key that changed
valuestringThe new value as base64-encoded bytes (empty for deletes)
revisionnumberNew revision number
operation"put" | "delete"What happened
bucketstringBucket name

event.value is the raw stored bytes encoded as base64 — not a parsed JSON object. If you stored JSON, decode with JSON.parse(atob(event.value)) before use.

Error Handling

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

ErrorHTTP StatusWhen
Key not found404Get or delete on a missing key
Key already exists412Create when the key already exists
Revision mismatch412Update with a stale revision
Bucket not found404Operations 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.