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
| Path | Handler |
|---|---|
GET /domain/:name + Upgrade: websocket | Authenticated domain socket. |
/, /app/* | Playground UI. |
/docs, /docs/:page | This documentation site. |
| other static files | Vite-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.