Skip to content
Built with

Durable state ​

Build & Deploy

Use durable state when requests must share data under one name, such as a shopping cart, a chat room or a rate limiter. The SDK gives you typed methods and keeps the state between calls, using Durable Objects.

On this pageDefine a durable state module · Call it from routes · Add WebSockets or alarms

Create a module in durable-objects/ with defineDurableState(). The SDK provides a SQLite-backed Cloudflare Durable Object for it. It works for native SDK apps on SDK platforms and direct Cloudflare deploys.

Commit void.lock.json when the SDK adds a resource. Keep its deployed migration entries in their original order.

Define state and methods ​

ts
// durable-objects/counter.ts
import { defineDurableState } from 'void/durable';

export const Counter = defineDurableState({
  initialState: { count: 0 },
  methods: {
    increment(context, amount: number) {
      context.state.count += amount;
      return context.state.count;
    },
    read(context) {
      return context.state.count;
    },
  },
});

export default Counter;

Default-export the object returned by defineDurableState().

Call it from a route ​

This step calls the object from a route. Import the definition and call get() with a name. Arguments and return values are typed.

ts
// routes/api/counter.ts
import { defineHandler } from 'void';
import { Counter } from '../../durable-objects/counter';

export const POST = defineHandler(async () => {
  const counter = Counter.get('global');
  const count = await counter.increment(1);
  return { count };
});

The same name always resolves to the same Durable Object. Use getById(id) when you already have a DurableObjectId. For advanced cases, both helpers accept a DurableObjectNamespace as the first argument.

Execution and persistence ​

The SDK loads state before the first operation. It runs RPC methods one at a time. After a method, fetch or alarm handler succeeds, it saves the state.

If the handler or save fails, the SDK restores context.state to its previous value. Direct writes to context.storage are not rolled back.

Each method receives:

  • context.state, which can be mutated or replaced
  • context.env, the Durable Object environment
  • context.id and context.storage
  • context.setAlarm() and context.deleteAlarm()

fetch, alarm and constructor are reserved method names. This step defines fetch and alarm as top-level hooks instead.

ts
export const Room = defineDurableState({
  initialState: { wakeups: 0 },
  methods: {},
  fetch(context) {
    return Response.json(context.state);
  },
  async alarm(context) {
    context.state.wakeups += 1;
    await context.setAlarm(Date.now() + 60_000);
  },
});

export default Room;

State migrations ​

Version the stored value when its shape changes. Versions start at 1 and must be contiguous. They run in order before an operation sees the state.

ts
export const Counter = defineDurableState({
  initialState: { count: 0, label: 'Counter' },
  version: 2,
  migrations: [
    {
      version: 1,
      migrate(old) {
        return { count: (old as { count: number }).count, label: 'Counter' };
      },
    },
    {
      version: 2,
      migrate(old) {
        return { ...(old as { count: number; label: string }), label: 'Total' };
      },
    },
  ],
  methods: {
    read(context) {
      return context.state;
    },
  },
});

export default Counter;

State migrations update your saved values. The SDK manages Cloudflare’s separate class migrations in void.lock.json.

For direct Cloudflare deployments, do not delete or reorder generated Durable Object migrations in void.lock.json after deployment. For a class addition, rename, or removal on an existing Worker, review the migration and run void deploy --platform cloudflare --atomic. If this Worker needs atomic publication on every deploy, set deploy: { cloudflare: { mode: 'atomic' } } in void.config.ts. Cloudflare applies class lifecycle changes in one deployment. The Worker receives traffic before the SDK checks readiness, and you cannot roll back across that migration boundary. Staged deploys keep pre-traffic readiness verification.

Rename a file while keeping its state ​

This step finds the current name before a rename.

sh
void info

Example output: Details about the project and its bindings.

For durable-objects/counter.ts, the SDK prints:

text
To preserve this resource when moving its code, add this to 'defineDurableState':
  name: "counter",

This step pins that name in the existing definition.

ts
export const Counter = defineDurableState({
  name: 'counter',
  initialState: { count: 0 },
  methods: {
    read(context) {
      return context.state.count;
    },
  },
});

export default Counter;

Rename the file within durable-objects/, update its imports, and deploy. Keep name: 'counter' and the names passed to Counter.get() unchanged. That preserves the stored state.

name is optional. Use a static string, either inline or in a local const, containing words that start with a letter, such as 'shopping-cart'. Names must produce distinct Worker classes and bindings. Changing an existing resource's name selects a different resource.

If you already moved the file, void info also shows resources recorded in void.lock.json without a matching definition. Identify the original resource and use its suggested name. If no name can be suggested, restore the original file from source history and run void info before moving it again. Keep the existing lock and migration history.

Deployment support ​

Typed state works in local development, on SDK platforms, and in direct Cloudflare deploys of native SDK apps. Use void deploy for an SDK platform or void deploy --platform cloudflare for your own Cloudflare account.

On an SDK platform, each class keeps its storage across deployments and rollbacks. A rollback restores the class code and keeps the current stored data. Removing a definition makes the class inactive. Adding it back with the same resource name restores its data. Deleting the project deletes its Durable Object storage.

A project supports up to 32 distinct custom classes, including removed classes whose storage is retained. Keep resource names and instance names stable to preserve data. Upgrade an older SDK installation before you deploy an app with custom Durable Objects.

Custom Durable Object modules are not supported in meta-frameworks or on Node.js, Bun, and Deno.

This is separate from typed WebSocket routes, which also use SQLite-backed Durable Objects and work on both direct Cloudflare and hosted SDK deployments.

Native Durable Object classes ​

This step keeps a native Cloudflare class with defineDurableObject, including classes that extend Cloudflare Agents. The SDK discovers its binding and exports the class without replacing its constructor, lifecycle, or RPC methods. Like Durable State, this supports the SDK platforms and direct Cloudflare deployments.

ts
// durable-objects/workspace.ts
import { DurableObject } from 'cloudflare:workers';
import { defineDurableObject } from 'void/durable';

class WorkspaceObject extends DurableObject {
  async fetch() {
    return new Response('Workspace');
  }
}

export const Workspace = defineDurableObject({
  name: 'workspace',
  class: WorkspaceObject,
});
export default Workspace;

Call Workspace.get('project-id') from a server handler to obtain its typed stub. Keep the resource name and instance name stable across deployments to retain storage.

Next steps ​

implemented.stream AI