State
Add durable key-value storage to your workers with a Cloudflare KV-compatible API.
State
Ploy State provides a durable key-value store backed by SQLite. The binding type
is still StateBinding, but it now implements the Cloudflare KVNamespace API
so existing Workers code can keep using get, put, delete,
getWithMetadata, and list with the same binding names as before.
Configuration
Add a state binding in your ploy.yaml:
kind: dynamic
build: pnpm build
out: dist
state:
STATE: defaultThe key (STATE) is the binding name available in your worker's env. The value (default) is the state store identifier.
Run ploy types to generate TypeScript types:
import type { StateBinding } from "@meetploy/types";
export interface Env {
STATE: StateBinding;
}StateBinding is a KV superset: it supports the Cloudflare KV API plus
Ploy-specific set() and atomic JSON update().
Basic Example
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === "/put") {
await env.STATE.put("greeting", JSON.stringify({ message: "hello" }), {
metadata: { source: "example" },
});
return Response.json({ success: true });
}
if (url.pathname === "/get") {
const value = await env.STATE.get("greeting", "json");
return Response.json({ value });
}
if (url.pathname === "/get-with-metadata") {
const result = await env.STATE.getWithMetadata("greeting", {
type: "json",
});
return Response.json(result);
}
if (url.pathname === "/delete") {
await env.STATE.delete("greeting");
return Response.json({ success: true });
}
return new Response("State Worker");
},
} satisfies Ploy;API Methods
get(key) - Get a Value
Returns the stored value or null if the key doesn't exist.
const value = await env.STATE.get("my-key");
// value is string | nullCloudflare-style typed reads are also supported:
const settings = await env.STATE.get("settings", {
type: "json",
cacheTtl: 60,
});
const blob = await env.STATE.get("avatar", "arrayBuffer");get(keys) - Bulk Get
Pass an array of keys to receive a Map, just like Cloudflare KV.
const values = await env.STATE.get(["user:1", "user:2"], { type: "json" });
const user1 = values.get("user:1");getWithMetadata(key) - Get Value and Metadata
const result = await env.STATE.getWithMetadata("settings", { type: "json" });
console.log(result.value);
console.log(result.metadata);put(key, value) - Store a Value
put() matches the Cloudflare KV write API. It supports strings, JSON blobs,
ArrayBuffer, typed arrays, and ReadableStream, plus metadata and expiration.
await env.STATE.put("username", "alice", {
expirationTtl: 3600,
metadata: { updatedBy: "admin" },
});set(key, value) - Set a Value
set() remains available for existing Ploy code. It is equivalent to storing a
plain string value without KV options.
await env.STATE.set("username", "alice");delete(key) - Delete a Value
Removes a key from the state store.
await env.STATE.delete("username");list() - List Keys
List keys with Cloudflare-compatible prefix, limit, and cursor options.
const page = await env.STATE.list({ prefix: "user:", limit: 100 });
console.log(page.keys);
console.log(page.cursor);update(key, update) - Atomic JSON Updates
Applies a MongoDB-style update document to a JSON value in a single atomic write. No need to read the full value, modify it in memory, and write it back -- operations are applied directly in SQLite using JSON functions.
await env.STATE.update("user:123", {
$set: { role: "admin", updatedAt: Date.now() },
});Five operators are supported:
| Operator | Description | Example |
|---|---|---|
$set | Set one or more fields | { $set: { name: "Alice" } } |
$unset | Remove one or more fields | { $unset: { oldField: "" } } |
$inc | Increment numeric fields | { $inc: { visits: 1 } } |
$push | Append a value to an array | { $push: { tags: "new-tag" } } |
$pop | Remove first (-1) or last (1) element from an array | { $pop: { queue: -1 } } |
Fields use dot notation for nested paths: "nested.field", "array.0".
Use put() when you want Cloudflare KV compatibility, metadata, expiration,
or binary payloads. Use update() when you want atomic partial updates to a
JSON document without a read-modify-write cycle.
Common Patterns
Storing JSON Data
await env.STATE.put(
"user:123",
JSON.stringify({ name: "Alice", role: "admin" }),
);
const user = await env.STATE.get("user:123", "json");Feature Flags
async function isFeatureEnabled(
env: PloyEnv,
feature: string,
): Promise<boolean> {
const value = await env.STATE.get(`feature:${feature}`);
return value === "true";
}
// Enable a feature
await env.STATE.put("feature:dark-mode", "true");
// Check the feature
const enabled = await isFeatureEnabled(env, "dark-mode");Persistent Counters
// Atomic increment — no read-modify-write needed
await env.STATE.update("counters", {
$inc: { pageViews: 1 },
});User Preferences
async function getUserPreferences(env: PloyEnv, userId: string) {
const raw = await env.STATE.get(`prefs:${userId}`);
return raw ? JSON.parse(raw) : { theme: "light", language: "en" };
}
async function setUserPreferences(
env: PloyEnv,
userId: string,
prefs: Record<string, string>,
) {
await env.STATE.put(`prefs:${userId}`, JSON.stringify(prefs));
}Atomic Partial Updates
When you store JSON objects and only need to change a few fields, use update() instead of reading the whole object, modifying it, and writing it back:
// Instead of this (read-modify-write):
const raw = await env.STATE.get("user:123");
const user = JSON.parse(raw!);
user.lastSeen = Date.now();
user.visits += 1;
await env.STATE.put("user:123", JSON.stringify(user));
// Do this (atomic update):
await env.STATE.update("user:123", {
$set: { lastSeen: Date.now() },
});This avoids race conditions when multiple requests update the same key concurrently, and is more efficient since only the changed fields are sent over the wire.
Counters with $inc
// Increment a counter atomically (no read-modify-write needed)
await env.STATE.update("user:123", {
$inc: { visits: 1, score: 10 },
});Queue Management with update()
// Append to a list
await env.STATE.update("job-queue", {
$push: { pending: { id: "job_1", task: "send-email" } },
});
// Remove the first item from a list
await env.STATE.update("job-queue", {
$pop: { pending: -1 },
});
// Multiple operations in one atomic call
await env.STATE.update("job-queue", {
$pop: { pending: -1 },
$push: { completed: { id: "job_1", finishedAt: Date.now() } },
});Multiple State Bindings
You can configure multiple state stores for different use cases:
state:
STATE: default
USER_STATE: users// General state
await env.STATE.put("config", value);
// User-specific state
await env.USER_STATE.put("user:abc", userData);Next Steps
How is this guide?
Last updated on