Skip to content

feat(celld): Durable Objects on AWS via denoland/celld - #1127

Open
sam-goodwin wants to merge 4 commits into
mainfrom
claude/alchemy-aws-lambda-integration-68e58d
Open

feat(celld): Durable Objects on AWS via denoland/celld#1127
sam-goodwin wants to merge 4 commits into
mainfrom
claude/alchemy-aws-lambda-integration-68e58d

Conversation

@sam-goodwin

@sam-goodwin sam-goodwin commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Self-hosted Durable Objects on AWS, built on celld. A top-level Celld namespace mirroring celld's own model: a Fleet is the infrastructure (nodes + an S3 bucket), it runs one Worker deployment, and the Worker contains the cells (Durable Objects). Hosting is a pluggable layer — AWS.providers() contributes the aws-ecs fleet host.

Start with a single file — a Fleet, a Worker deployed onto it, and an inline cell; the Worker's fetch runs on the fleet's nodes with native access to the cells:

// cells.ts
export class Cells extends Celld.Fleet<Cells>()("Cells", {
  instances: 2,
}) {}

export class Counter extends Celld.DurableObject<Counter>()(
  "Counter",
  Effect.gen(function* () {
    const state = yield* Celld.DurableObjectState;
    return Effect.gen(function* () {
      const count = (yield* state.storage.get<number>("count")) ?? 0;
      return {
        increment: () =>
          Effect.gen(function* () {
            const next = count + 1;
            yield* state.storage.put("count", next);
            return next;
          }),
      };
    });
  }),
) {}

export default class CellsWorker extends Celld.Worker<CellsWorker>()(
  "CellsWorker",
  { fleet: () => Cells, main: import.meta.url },
  Effect.gen(function* () {
    const counters = yield* Counter;
    return {
      fetch: Effect.gen(function* () {
        const request = yield* HttpServerRequest;
        const room =
          new URL(request.url, "http://cells").pathname.slice(1) || "lobby";
        const value = yield* counters.getByName(room).increment();
        return yield* HttpServerResponse.json({ room, value });
      }),
    };
  }),
) {}

As the app grows, split the cell from the Worker — the class becomes a pure tag and the layer binds the implementation to the Worker that hosts it:

// counter.ts
export class Counter extends Celld.DurableObject<Counter>()("Counter") {}

export const CounterLive = Counter.make(
  CellsWorker,
  Effect.gen(function* () {
    /* same two-phase impl as above */
  }),
);
// main.ts — the deployable Worker module hosts the class by providing the layer
export default CellsWorker.make(
  { fleet: Cells, main: import.meta.url },
  Effect.gen(function* () {
    const counters = yield* Counter;
    return { fetch: /* … */ };
  }).pipe(Effect.provide(CounterLive)),
);

Running the same class on a different Worker is just a different layer, and any other binding host connects the same way — a Lambda Function provides Counter.client(CellsWorker) (network attachment + connection secrets bind automatically) and calls cells over the fleet gateway:

export const CounterOnEdge = Counter.make(EdgeWorker, impl);

// api.ts — an AWS Lambda caller
Effect.gen(function* () {
  const counters = yield* Counter;
  const n = yield* counters.getByName("room-1").increment();
  // ...
}).pipe(Effect.provide(Counter.client(CellsWorker)));

RPC methods behave like local Effects — Stream-returning methods arrive as Streams, and Effect.fail decodes as the typed failure on the caller.

🤖 Generated with Claude Code

sam-goodwin and others added 4 commits August 6, 2026 13:56
Top-level, host-agnostic Celld provider mirroring the Cloudflare
DurableObject DX. The DO class is a pure tag; the layer binds the
implementation to a fleet (Counter.make(Cells, impl)) and dual-dispatches
at build time: hosting inside its fleet, remote RPC stub on any other
host. Fleets deploy the same Worker bundle Cloudflare does, plus an
authenticated RPC gateway; celld deploy is a pure bucket write via a
pinned CLI. Hosting is pluggable behind the Celld.FleetHost keyed-tag
seam (the Kubernetes.ClusterAdapter pattern); AWS.providers() contributes
the aws-ecs host: S3 bucket, no-NAT VPC with S3 gateway endpoint,
Cloud Map discovery, Fargate nodes with an entrypoint that supervises
celld and exports ECS task-role credentials (celld v0.1.0's write
replication only resolves creds via EC2 IMDS).

Live-verified end to end: VPC-attached effectful Lambda driving cells
over the fleet gateway — write persistence, per-cell isolation, NDJSON
streaming, and typed error decoding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The inline form couples the implementation to the class — hosted by
whichever fleet yields it, with callers selecting the fleet via
Counter.client(fleet). The tagged form's layer (Counter.make(fleet,
impl)) remains the scalable path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Celld's own model: a fleet is the nodes + bucket and runs exactly one
Worker deployment, which contains the Durable Objects. The resources now
mirror that: Celld.Fleet composes the infrastructure through the
FleetHost seam; Celld.Worker (deploys onto a fleet via `fleet:`) owns the
bundle + celld deploy + node rolls and hosts the DO classes —
Counter.make(CellsWorker, impl) binds implementations to the Worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
celld deploy hard-requires an esbuild binary (its own bundling step —
alchemy's artifacts are rolldown-built; the captured invocation is a
non-minifying single-file flatten of our chunk graph with node:*/
cloudflare:* external). Only Celld deploys need it, so it moves to an
optional peerDependency pinned through the workspace catalog, with a
devDependency for the repo's own tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant