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

Patterns

Practical recipes for common KV Store use cases.

A write to a bucket that does not exist returns 404. Create each bucket first, as the caching and session recipes do. The feature-flag and counter recipes assume feature-flags and counters already exist.

Use TTL buckets to cache computed or fetched data. Keys auto-expire after the configured duration, so stale data is cleaned up automatically. On a cache miss, recompute the value and store it for future reads.

import { createClient, IronflowError } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });
const kv = client.kv();
// Create a bucket with 5-minute TTL
await kv.createBucket({ name: "cache", ttlSeconds: 300 });
const cache = kv.bucket("cache");
async function getCached(key: string, compute: () => Promise<unknown>) {
try {
const entry = await cache.getJSON(key);
return entry.value;
} catch (error) {
if (!(error instanceof IronflowError)) throw error;
// A missing key or an entry that is not JSON is a miss: recompute and overwrite it.
if (error.status !== 404 && error.code !== "INVALID_KV_JSON") throw error;
const value = await compute();
const encoded = JSON.stringify(value);
if (encoded !== undefined) await cache.put(key, encoded);
return value;
}
}
const profile = await getCached("user.123.profile", async () => {
return fetchUserProfile("123");
});

Store feature flags as JSON values in a dedicated bucket. Read flags at runtime to control application behavior. Updates take effect immediately without redeployment.

import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });
const flags = client.kv().bucket("feature-flags");
// Set a flag
await flags.put("dark-mode", { enabled: true, rollout: 0.5 });
// Check a flag
const flag = await flags.get("dark-mode");
const flagData = JSON.parse(Buffer.from(flag.value as string, "base64").toString("utf-8"));
if (flagData.enabled) {
enableDarkMode();
}
// List all flags
const keys = await flags.listKeys();
for (const key of keys) {
const entry = await flags.get(key);
console.log(`${key}: ${Buffer.from(entry.value as string, "base64").toString("utf-8")}`);
}

Store per-user session data in a TTL bucket so sessions expire automatically. Use key naming conventions like user.<id> to organize entries.

import { createClient } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });
const kv = client.kv();
// Sessions expire after 1 hour
await kv.createBucket({ name: "sessions", ttlSeconds: 3600 });
const sessions = kv.bucket("sessions");
// Store session data
await sessions.put("user.abc-123", {
userId: "abc-123",
role: "admin",
lastActiveAt: new Date().toISOString(),
});
// Retrieve session
try {
const session = await sessions.get("user.abc-123");
const sessionData = JSON.parse(Buffer.from(session.value as string, "base64").toString("utf-8"));
console.log(sessionData.role); // "admin"
} catch {
// Session expired or doesn't exist
redirectToLogin();
}
// Refresh session on activity (resets TTL)
await sessions.put("user.abc-123", {
...existingSession,
lastActiveAt: new Date().toISOString(),
});
// Explicit logout
await sessions.delete("user.abc-123");

Use compare-and-swap (update) for concurrent-safe modifications. Read the current value and its revision, apply your change, then write back with the original revision. If another writer modified the key in between, the update fails and you retry.

import { createClient, IronflowError } from "@ironflow/node";
const client = createClient({ serverUrl: "http://localhost:9123" });
const counters = client.kv().bucket("counters");
// Initialize counter
await counters.put("page-views", 0);
async function incrementCounter(key: string): Promise<number> {
while (true) {
const entry = await counters.getJSON(key);
if (typeof entry.value !== "number") throw new Error("Expected a counter number");
const count = entry.value + 1;
try {
await counters.update(key, count, entry.revision);
return count;
} catch (error) {
if (!(error instanceof IronflowError) || error.status !== 412) throw error;
// Revision mismatch — another writer won, retry
continue;
}
}
}
const newCount = await incrementCounter("page-views");
console.log(`Page views: ${newCount}`);