Skip to content

Cloudflare

Host your cluster on Cloudflare: one Worker runs every class you give it, on Durable Objects. There is no database and nothing to scale. Callers reach it through the Worker’s gateway.

src/Cluster.ts
import * as ClusterCloudflare from "@alchemy.run/effect-cluster/Cloudflare";
export const Default = ClusterCloudflare.DurableObjectPool("Default");

Install @alchemy.run/cloudflare and finish Cloudflare setup. Add its providers next to the cluster’s:

alchemy.run.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Cluster from "@alchemy.run/effect-cluster";
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import Api from "./src/Api.ts";
import RoomLive from "./src/RoomLive.ts";
export default Alchemy.Stack(
"Rooms",
{
providers: Layer.mergeAll(Cloudflare.providers(), Cluster.providers()),
state: Cloudflare.state(),
},
Effect.gen(function* () {
const api = yield* Api;
return { url: api.url };
}).pipe(Effect.provide(RoomLive)),
);

Your entities, workflows, singletons and crons don’t change. Their implementations take Default like they take any pool.

A Durable Object is already one instance per name, in one place, handling one event at a time. That’s what Effect Cluster’s shards and runners exist to give you, so on Cloudflare there are none. Every entity type shares one Durable Object class, and each instance is the object named by its type and id. This is the design of Effect’s own Durable Object support.

lobby.Join()CallersWorker, Lambda, containerPool Workerthe gatewayRoom lobbyDurable ObjectRoom kitchenDurable Object
The Worker routes each call to the object named by the entity's type and id.

One entity calling another goes straight from object to object, without the gateway.

An instance keeps its state in memory, as on runners, until it has been idle for its maxIdleTime. Cloudflare puts an object to sleep after about ten seconds without events, so until then the object’s alarm wakes it every few seconds. A short maxIdleTime costs fewer alarms:

src/RoomLive.ts
import * as Effect from "effect/Effect";
import * as Ref from "effect/Ref";
import { Default } from "./Cluster.ts";
import { Room } from "./Room.ts";
export default Room.make(
{ main: import.meta.url, maxIdleTime: "15 seconds" },
Effect.gen(function* () {
return Effect.gen(function* () {
const members = yield* Ref.make(new Set<string>());
return Room.of({
Join: ({ payload }) =>
Ref.modify(members, (all) => {
const next = new Set(all).add(payload.name);
return [next.size, next];
}),
Members: () => Effect.map(Ref.get(members), (all) => [...all]),
});
});
}).pipe(Effect.provide(Default)),
);

A persisted message is written to the object’s own SQLite before it runs, and its reply after. A primary key runs once, and a message cut short by a restart runs again when the object’s alarm fires.

Each workflow execution is a Durable Object of its own, running Alchemy’s Durable Object workflow engine: activities, durable sleeps and signals are kept in its SQLite.

A singleton runs in an object that its alarm checks on every few seconds, restarting the fiber if the object restarted. A cron runs in an object that its alarm wakes at each scheduled time, down to the second.

A Worker can’t fetch another Worker’s workers.dev address in the same account, so a Worker that yields one of your classes gets a service binding to the pool’s Worker, and its calls go through that:

src/Api.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Room } from "./Room.ts";
export default class Api extends Cloudflare.Worker<Api>()(
"Api",
{ main: import.meta.url },
Effect.gen(function* () {
const rooms = yield* Room;
return {
fetch: Effect.gen(function* () {
const lobby = yield* rooms.getByName("lobby");
return yield* HttpServerResponse.json({ members: yield* lobby.Join({ name: "sam" }) });
}).pipe(Effect.orDie),
};
}),
) {}

Callers on any other runtime, like a Lambda function or a container, use the Worker’s workers.dev address, with the token Alchemy binds into them.

An existing Effect Cluster program needs Node: give it a pool on another host. Shard groups don’t apply, because there are no shards.

Every option, with its default, is in the reference: DurableObjectPool.