Skip to content

Effect Cluster

Effect Cluster runs stateful, addressable programs across a group of processes. An entity has one instance per id, like a chat room or a cart, and handles its messages one at a time, so it keeps state without locks. Instances are spread over runners by shard, and a Postgres database records which runner owns each shard and keeps the messages that must survive a restart. Workflows persist each step and resume on another runner, singletons run one fiber across the cluster, and crons run once per scheduled time.

With Alchemy, the database and a pool of runners deploy in the same Stack as the rest of your app, on Docker, AWS, GCP, Kubernetes, Fly or Hetzner. On Cloudflare there is no database: each instance is a Durable Object.

New to Effect Cluster? Start with What is Effect Cluster?.

A pool of runners, and the database they coordinate through:

src/Cluster.ts
import * as ClusterDocker from "@alchemy.run/effect-cluster/Docker";
import * as Layer from "effect/Layer";
export const Database = ClusterDocker.Database("Cluster");
export const Default = ClusterDocker.SwarmPool("Default").pipe(Layer.provide(Database));

An entity, declared with its messages and its implementation, on that pool:

src/Room.ts
import * as Cluster from "@alchemy.run/effect-cluster";
import { Entity } from "effect/cluster";
import * as Effect from "effect/Effect";
import * as Ref from "effect/Ref";
import { Rpc } from "effect/rpc";
import * as Schema from "effect/Schema";
import { Default } from "./Cluster.ts";
const Rooms = Entity.make("Room", [
Rpc.make("Join", { payload: { name: Schema.String }, success: Schema.Int }),
]);
export default class Room extends Cluster.Entity<Room>()(
Rooms,
{ main: import.meta.url },
Effect.gen(function* () {
return Effect.gen(function* () {
const members = yield* Ref.make(new Set<string>());
return Rooms.of({
Join: ({ payload }) =>
Ref.modify(members, (all) => {
const next = new Set(all).add(payload.name);
return [next.size, next];
}),
});
});
}).pipe(Effect.provide(Default)),
) {}

Any runtime calls a room by its id:

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

Swap ClusterDocker for ClusterAWS, ClusterFly or another cloud, and the room doesn’t change.

Want to build? Start with Setup, then the tutorial builds chat rooms step by step, from a room on your machine to invitations, a nightly cleanup and AWS.

Looking for something specific? Every section of these docs is listed below, and every resource is in the generated API reference.