Skip to content

Register and mount

host.durableObject, per-DO routes, and mount() on the Worker front door.

AI generated, pending review Updated 10 days ago · Flare 0.3

Register a Durable Object class on the host, add routes on the returned handle, then mount that DO at a URL prefix on the Worker front door.

import { FlareHost, FlareResponse } from "@flare-ts/core";
import { cf, DurableState, FlareDurableObject } from "@flare-ts/core/cloudflare";
import { str } from "@flare-ts/lib/schema";
class Room extends FlareDurableObject {
public static override deps = [DurableState] as const;
}
const host = new FlareHost(cf);
const room = host.durableObject(Room, { binding: "ROOM" });
room.http.get(
"/meta",
{ inject: { ds: DurableState } },
(_ctx, scope) => new FlareResponse(200, { id: scope.ds.id.toString() }),
);
room.ws.route("/live/:topic", { params: { topic: str } })
.message((ws, scope) => ws.send(scope.input.message.raw));
room.mount("/rooms/:name");

binding names the env key Wrangler uses (defaults to the class name). HTTP and WebSocket traffic under /rooms/... resolves to a DO instance named by the trailing :name segment.

  • Path must start with / and be non-empty
  • No wildcard segments in the mount path (the mount adds its own routing)
  • A param-trailing mount (/rooms/:name) takes the instance name from the trailing parameter; a literal-trailing mount (/api/me) needs resolve(...) registered on the handle before build(), or the build fails with MOUNT_REQUIRES_RESOLVE
  • A mount’s subtree is owned exclusively by the DO: overlap with any front-door route, group prefix, or other mount fails build() with MOUNT_ROUTE_CONFLICT

Do not call the raw DO binding’s fetch() from application code: nothing strips a client-forged state envelope on that path. Route DO traffic through .mount() or durable(). durable(namespace, name).fetch() is a state-free raw tunnel by design (it strips the reserved framework state headers); durable(...).forward(ctx, Class) is the state-carrying call.

A literal-trailing mount (/api/me) has no :name segment to name the instance, so register resolve(...) on the handle. Two overloads: resolve(handler) with no injected deps, or resolve({ inject }, handler) for typed DI. The return value decides what happens:

  • a string is the DO instance name (the request forwards to it);
  • a FlareResponse short-circuits (it is returned; no DO is entered);
  • a throw goes through the normal error pipeline.

resolve runs in the Worker front-door context, so it can inject front-door services (auth, session, Bindings). A DurableState-dependent service fails host.build().

import { FlareResponse } from "@flare-ts/core";
room.mount("/api/me"); // literal-trailing: needs resolve()
room.resolve({ inject: { sessions: Sessions } }, (ctx, scope) => {
const user = scope.sessions.current(ctx);
if (!user) return new FlareResponse(401);
return user.id; // the DO instance name
});
[[durable_objects.bindings]]
name = "ROOM"
class_name = "Room"
[[migrations]]
tag = "v1"
new_classes = ["Room"]

Export the DO class from the same module as export default app.export().

host.durableObject(Class, builder) (or (Class, opts, builder)) invokes the builder immediately with the same handle, so HTTP, WebSocket, mount, and resolve can be registered in one co-located block.

import { FlareHost, FlareResponse } from "@flare-ts/core";
import { cf, FlareDurableObject } from "@flare-ts/core/cloudflare";
class Room extends FlareDurableObject {
public static override deps = [];
}
const host = new FlareHost(cf);
host.durableObject(Room, (room) => {
room.http.get("/ping", () => new FlareResponse(200, { ok: true }));
room.ws.route("/live");
room.mount("/rooms/:name");
});

The handle returned from host.durableObject() is the same in all forms.