Reference

Host

The included host is a Cloudflare Worker plus one SQLite-backed Durable Object class. It authenticates upgrades, persists the log, enforces application policy, and emits bounded telemetry. It does not interpret document bodies.

Routes

PathHandler
GET /domain/:name + Upgrade: websocketAuthenticated domain socket.
/, /app/*Playground UI.
/docs, /docs/:pageThis documentation site.
other static filesVite-built assets from playground/dist.

wrangler.jsonc runs the Worker first for /domain/*, /app, and /docs so pretty routes work in production. Local Vite uses the same mapping.

ws(s)://host/domain/ws%2Fdemo%2Fchannel%2Fgeneral?as=ada&workspace=demo

The name is URL-encoded because domain ids contain slashes. Query parameters are only for the demo identity adapter.

Factories

const DomainBase = createDomainHost(application, env => env.DOMAIN, observe);
export class Domain extends DomainBase {}
const upgrade = createUpgradeHandler(application, env => env.DOMAIN, observe);

src/host/index.ts binds the playground application. A different app supplies its own createYttrium config and the same two factories. Pass identical observability options to both.

Persistence

Each domain's home is that Durable Object's SQLite. The host writes the commit log before updating memory and before fanout. Snapshots and compaction stay in the same SQL. Idle objects remain on the DO — a shelf, not a duration. There is no upload of the tree to R2 on sleep.

R2 is reserved for External blobs referenced from the tree as { key, hash, size }. Clients fetch those objects directly. The DO must not stream a blob through its one thread.

Policy replicas use a policy_cache table outside the content log. After wake, that cache is unavailable until the feed re-sends hello and catches up. Content authorization waits for that barrier.

Trusted initialize(domain) mutations run once before a new domain becomes accessible. The demo uses this to seed Ada/Beau and the starter catalog. Application policy must keep unknown resources closed. Cross-domain provisioning is application work and must tolerate partial completion.

Hibernation

Incoming sockets serialize an attachment: auth session, client id, subscription id, and private-view revision floor. Cloudflare keeps the sockets and drops heap. On restore the host rehydrates SQL, rebinds attachments, and resynchronizes policy. It does not automatically restore live stream prefixes or the in-memory presence map.

Only accepted sockets hibernate; sockets a DO dials do not. So a content DO never dials its policy domain. It asks the policy domain to dial it (binding-only openPolicyFeed RPC) and accepts that socket, so content DOs can hibernate while idle clients stay connected, and a policy commit wakes them. The cost moves to the policy domain: it holds one outgoing socket per active dependent and stays awake while any dependent has clients. That is one DO per workspace instead of one per channel. The feed closes when the dependent's last client leaves. If a live feed drops, the dependent fails closed, re-dials, and only then re-checks its clients.

Observability

Observability is a host-side plane. It never writes domain state. Sink failures never change a protocol response. With no configuration, each event is structured JSON on console.log. Workers Logs and wrangler tail are the default viewers. Platform head sampling stays at 1 so Yttrium can sample at the event level.

const observe = {
  observability: {
    emit(event) { console.log(JSON.stringify(event)); },
  },
  user: ({ principal }) => principal.accountId,
  sampleRate: 0.1,
};

user must return an opaque label from trusted authentication state, never a client-claimed id. sampleRate applies only to successful commit and catch_up (default 1). Required events always carry sample_rate: 1: wake, denied, nack, decode_failed, hello_failed, compact, host_failed.

Events may include domain, durable object id, user label, frame type, persist type, commit or mutation ids, peer count, and send-attempt count. They never include operation bodies, snapshots, principals, tokens, raw frames, presence payloads, stream bytes, or arbitrary exception text. Successful presence and stream traffic stays quiet.

A custom sink may be (env) => sink when it needs a binding. The host does not await telemetry I/O. Cloudflare can also export the same logs through platform OpenTelemetry or Logpush without adding a logging domain.

Run and deploy

npm test
npx tsc --noEmit
npm run build
npm run worker
npm run test:host          # real WebSockets, disposable domains
YTTRIUM_TEST_ORIGIN=http://localhost:8789 npm run test:host
npm run deploy

test:host covers forged context, service isolation, two tabs, private views, role updates, prefix grants, revocation, and reconnects. Unit tests cover missing inputs, hook failures, expiry, cache barriers, gaps, and gateway header replacement.

Set VITE_LIVE_HOST if the playground should talk to a Worker that is not localhost:8787 in dev.