Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

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.

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

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

const entry = await bucket.getJSON("user.123");
console.log(entry.key); // "user.123"
console.log(entry.value); // { name: "Alice", role: "admin" }
console.log(entry.revision); // 1
console.log(entry.created_at); // "2026-02-22T10:00:00Z"
console.log(entry.operation); // "put"

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 "not_found"

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

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.

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);
}

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.getJSON("user.123");
const current = entry.value;
if (typeof current !== "object" || current === null || Array.isArray(current)) {
throw new Error("Expected a user object");
}
// 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);
}

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

// 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

Watch provides real-time notifications when keys are created, updated, or deleted. Changes are delivered over WebSocket and are available in the Node, browser, Go, and Python SDKs. When a watch opens, the server first replays the current state of each matching key, then sends live changes.

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

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

ErrorHTTP StatusWhen
Key not found404Get on a missing or deleted key (deleting a missing key succeeds)
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 carries the HTTP status in err.status
if (err.status === 412) {
console.error("Key already exists — use put() to overwrite or update() with a revision");
} else if (err.status === 404) {
console.error("Bucket not found — create it first");
} else {
console.error("Ironflow error:", err.message);
}
} else {
console.error("Unexpected error:", err);
}
}

failed_precondition also covers the database size limit (HTTP 507). To tell a failed create or update (HTTP 412) from it, check the status: err.status === 412 in TypeScript, ironErr.Details["http_status"] == 412 in Go.