Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/denoland/celld/llms.txt

Use this file to discover all available pages before exploring further.

Celld implements the Workers JS RPC system: WorkerEntrypoint and RpcTarget from cloudflare:workers, named entrypoints on service bindings, and method calls on Durable Object stubs. Arguments and returns use structured clone. Values that cannot be cloned — functions, streams, RpcTarget instances — travel as stubs: the receiver gets a handle, and calling it runs the code back where it came from.
RPC methods on Durable Object stubs require either extends DurableObject in the class declaration or the js_rpc compatibility flag in wrangler.jsonc.

Basic RPC

A Durable Object exposes RPC methods by extending DurableObject and calling super(state, env). The Worker calls those methods directly on the stub — no fetch(), no URL routing:
import { DurableObject } from "cloudflare:workers";

export class Account extends DurableObject {
  constructor(state, env) {
    super(state, env);
    this.state = state;
  }

  async deposit(amount) {
    const balance = ((await this.state.storage.get("balance")) ?? 0) + amount;
    await this.state.storage.put("balance", balance);
    return balance;
  }

  async balance() {
    return (await this.state.storage.get("balance")) ?? 0;
  }

  // Cloneable in, cloneable out — the shape a cross-isolate call needs.
  async depositMany(amounts) {
    const running = [];
    for (const amount of amounts) running.push(await this.deposit(amount));
    return { total: running.at(-1), running };
  }
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const account = env.ACCOUNT.getByName("acct-1");

    switch (url.pathname) {
      case "/deposit":
        return Response.json({ balance: await account.deposit(10) });
      case "/deposit-many":
        return Response.json(await account.depositMany([1, 2, 3]));
      default:
        return Response.json({ balance: await account.balance() });
    }
  },
};

Arguments and returns

RPC arguments and return values are transferred using structured clone. Types that structured clone supports — plain objects, arrays, Map, Set, ArrayBuffer, Date — cross without issue. Values that cannot be cloned travel as stubs:
Value typeHow it travels
A plain functionBecomes a callable stub
A ReadableStreamBecomes a stream stub
An RpcTarget subclassBecomes an RpcTarget stub
The receiver holds the stub for the lifetime of the RPC call. Dispose of stub resources with using to release the underlying handle promptly.

Named entrypoints

A WorkerEntrypoint is a second, separately addressable interface on the same Worker. Reach it in-process with ctx.exports, or from another project with a service binding by name:
import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers";

class Receipt extends RpcTarget {
  constructor(id, balance) {
    super();
    this.id = id;
    this.balance = balance;
  }
  describe() {
    return `${this.id} holds ${this.balance}`;
  }
}

export class Ledger extends WorkerEntrypoint {
  add(left, right) {
    return left + right;
  }

  // A returned function becomes a stub — a callback factory.
  adder(left) {
    return (right) => left + right;
  }

  // A function argument arrives as a stub — the callee calls back into the caller.
  async tally(values, onEach) {
    let total = 0;
    for (const value of values) {
      total += value;
      await onEach(value, total);
    }
    return total;
  }

  // Returning an RpcTarget lets the caller pipeline method calls on the result.
  open(id, balance) {
    return new Receipt(id, balance);
  }
}

Promise pipelining

When a method returns an RpcTarget, the caller can pipeline a further method call on the result without waiting for the first call to resolve. Both hops leave together:
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    switch (url.pathname) {
      // ctx.exports reaches Ledger in this same Worker (same isolate).
      case "/entrypoint":
        return Response.json({ sum: await ctx.exports.Ledger.add(20, 22) });

      // adder() returns a function stub; calling it runs in the entrypoint.
      case "/adder": {
        using addFive = await ctx.exports.Ledger.adder(5);
        return Response.json({ sum: await addFive(37) });
      }

      // tally() receives a callback stub; the entrypoint calls back into the Worker.
      case "/callback": {
        const steps = [];
        const total = await ctx.exports.Ledger.tally([1, 2, 3], (v, running) => {
          steps.push({ v, running });
        });
        return Response.json({ total, steps });
      }

      // open() returns an RpcTarget stub; `using` disposes it when done.
      case "/receipt": {
        using receipt = await ctx.exports.Ledger.open("acct-1", 100);
        return Response.json({ describe: await receipt.describe() });
      }

      // Pipeline: no await on open(), so both hops travel together.
      case "/pipeline": {
        const describe = await ctx.exports.Ledger.open("acct-9", 7).describe();
        return Response.json({ describe });
      }
    }
  },
};
ctx.exports covers the entrypoints that the wrangler.jsonc configuration declares. It gives no stub for an undeclared Durable Object class.

Current limits

Stubs cannot cross isolate boundaries yet. A Durable Object is its own isolate. If you pass a function to a DO method, or return an RpcTarget from one, the call throws:
RPC stubs cannot cross isolate boundaries yet
To use callbacks and RpcTarget pipelining, keep the call within the same isolate by going through ctx.exports.
ScenarioWhat worksWhat does not work
DO stub (cross-isolate)Plain method calls with structured-cloneable args and returnsfetch(), awaitable properties, pipelined paths, function args, RpcTarget returns
Named entrypoint via ctx.exports (same-isolate)Full surface: method calls, callbacks, RpcTarget, pipelining
Named entrypoint via a cross-isolate service bindingSingle method calls with structured-cloneable valuesfetch(), awaitable properties, pipelined paths
The wrangler.jsonc for the RPC example:
{
  "name": "rpc",
  "main": "index.js",
  "compatibility_date": "2026-01-01",
  "durable_objects": { "bindings": [{ "name": "ACCOUNT", "class_name": "Account" }] },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Account"] }]
}

Build docs developers (and LLMs) love