Skip to content

Rivet.DurableObject reference

Source: packages/rivet/src/DurableObject.ts

A Durable Object on Rivet: a single-threaded, stateful instance per name, with its own SQLite database, key-value storage and alarm on the Rivet control plane. Each instance runs as a Rivet actor.

The implementation has the same two phases as Cloudflare.DurableObject: the outer Effect runs once per node and resolves bindings, and the inner Effect runs once per instance and returns its RPC methods, fetch and alarm.

Where it runs is a pool Layer provided to the outer Effect — RivetDocker.SwarmPool("Default"), RivetAWS.EcsPool("Default") — and the pool is given the control plane. Every Durable Object given the same pool runs on the same nodes.

yield* Counter from any runtime — a Cloudflare Worker, a Lambda, a container, another Durable Object — returns its namespace. Calls go through the control plane’s gateway over plain fetch, which creates the instance in its pool on first use, so callers need no RivetKit and no route to the nodes.

Given only main, it is external: a RivetKit program, async or @rivetkit/effect, that its pool bundles and runs as it is. It needs a pool of its own, and has no bindings or yield* calls.

An instance’s calls run in parallel, storage included: unlike Cloudflare, there is no input gate. Make a read-then-write atomic with state.storage.transaction(...), or provide Workers.InputGate (from alchemy/Workers) to run code written for Cloudflare’s gate unchanged.

export const Cluster = RivetDocker.SwarmControlPlane("Rivet");
export const Default = RivetDocker.SwarmPool("Default").pipe(Layer.provide(Cluster));
export default class Counter extends Rivet.DurableObject<Counter>()(
"Counter",
{ main: import.meta.url },
Effect.gen(function* () {
return Effect.gen(function* () {
const state = yield* Rivet.DurableObjectState;
return {
// Calls run in parallel: the transaction keeps the read and
// the write together.
increment: () =>
state.storage.transaction(
Effect.gen(function* () {
const count = ((yield* state.storage.get<number>("count")) ?? 0) + 1;
yield* state.storage.put("count", count);
return count;
}),
),
};
});
}).pipe(Effect.provide(Default)),
) {}
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const counters = yield* Counter;
return {
fetch: Effect.gen(function* () {
const count = yield* counters.getByName("visits").increment();
return HttpServerResponse.text(String(count));
}),
};
}),
) {}

DurableObject: Declaring and implementing separately

Section titled “DurableObject: Declaring and implementing separately”
export class Counter extends Rivet.DurableObject<Counter, CounterShape>()("Counter") {}
export const CounterLive = Counter.make(
{ main: import.meta.url, handler: "CounterLive" },
Effect.gen(function* () {
return Effect.gen(function* () {
const state = yield* Rivet.DurableObjectState;
return {
get: () => state.storage.get<number>("count").pipe(Effect.map((n) => n ?? 0)),
};
});
}).pipe(Effect.provide(Default)),
);
export const Counters = RivetDocker.SwarmPool("Counters").pipe(Layer.provide(Cluster));
export const Counter = Rivet.DurableObject("Counter", { main: "./src/actors.ts" }).pipe(
Effect.provide(Counters),
);