Reference
Access
Applications define authentication and permission rules. Yttrium binds identities, replicates declared policy inputs, enforces decisions locally, and delivers private derived views. There is no per-action authorization RPC and no freshness heartbeat.
Boundary
| Application owns | Yttrium provides |
|---|---|
| Identity provider, principal contents, origins, logout/refresh | Authenticated upgrade routing and bound session context |
| ACL shapes, roles, grants, selectors, trusted permission code | Declared policy replicas and input/output enforcement |
| Safe UI permission information and its shape | Private subscription views, replacements, revisions |
| Meaning of repeated user actions | Existing transport retries, authorized before processing |
Interfaces live in src/access/. A client id is never authority. Hello ids are transport bookkeeping. Directed replies go to the originating socket even if another socket claims the same client id.
createYttrium
const application = createYttrium({
authenticate: (request, env) => authenticateWithYourProvider(request, env),
serviceSession: (sourceDomain, policyDomain, env) =>
serviceIdentityFor(sourceDomain, policyDomain, env),
access: (domain) => ({
principalKey: (principal) => stableAccountOrServiceKey(principal),
policyDependencies: () => policyDomainsFor(domain),
authorize: (context) => can(context),
sessionValid: (session) => sessionStillValidLocally(session),
privateViews: viewsFor(domain),
onDecision: (event) => observeDecision(event),
}),
serverOptions: (domain) => ({ shape: shapeFor(domain) }),
initialize: (domain) => initialMutations(domain),
});
authenticate returns { principal, expiresAt? } or null. WorkOS, Better Auth, Supabase, cookies, or another scheme can sit behind that hook. The application validates permitted origins and must not treat a browser-supplied identity as proof.
principalKey must be stable across issuers and tenants. Browser storage must use the same account scope; this hook does not configure ClientStore automatically. Principal identity is not a retry identity.
authorize, sessionValid, private derivation, and validation are synchronous trusted server code. They must be bounded and perform no network I/O. Missing policy, exceptions, and non-boolean decisions fail closed as unavailable. An explicit false is denial.
Authorization context is { principal, domain, state, policies, request }. Request kinds are subscribe, receive (raw inbound frame), send (raw outbound frame), and private_view. Protect policy, author, owner, and selector fields explicitly. Generic write permission does not imply they are mutable.
Host session lifecycle
The public gateway constructs a fresh internal request and replaces any caller-supplied trusted-context header. Only trusted server code with the Durable Object binding constructs service context. The DO verifies its routed domain, then binds the principal to a hibernation attachment.
Enforcement covers subscription admission, all client protocol lanes, shared output per recipient, and private-view disclosure. Required inputs are checked before application policy. Every protected operation rechecks expiry and local session validity, including outbound updates.
Authentication refresh is a new authenticated connection. There is no in-band message that changes a socket's principal. Expired credentials close with 4001, denied subscriptions with 4004, and temporarily unavailable policy with 4003.
A denied mutation receives a terminal nack; the outbox discards it. Missing policy, hook failure, and expired credentials close or pause without nacking pending edits. Subscription denial prevents reading and stops automatic reconnect until the application retries.
A cold domain may accept an authenticated transport while policy synchronizes; it sends no protected content until the barrier and authorization succeed. A warm denied upgrade can return HTTP 403 immediately. Socket establishment alone does not grant read access.
ACL domains
An ACL is ordinary shaped Yttrium data. Policy edits use the same authorized mutation path as content. Each content DO subscribes to declared policy domains as its own service principal; the policy domain dials the feed and the content DO accepts it (see Host). The ACL authority checks that identity and scope. It never borrows a user's permissions or forwards internal ACL state to ordinary browsers. Administrators may separately subscribe to the ACL for editing.
PolicyReplica is a quiet, read-only Session. Its cache lives in the content DO's policy_cache SQLite table, outside the content log. A cached snapshot is unavailable until a fresh subscription synchronizes. The replica becomes ready only after shapes and a contiguous apply through synced.commit. A gap, malformed frame, or service denial removes readiness. Callbacks from replaced sockets cannot revive it.
The first implementation requires fully materialized policy inputs: record/tree shapes with keep_last: null. Elided, external, and mounted policy bodies fail closed. Multiple dependencies are supported, but their heads are not an atomic cross-domain snapshot.
Normal propagation targets below one second, with five seconds as an operational target, not a hard bound during silent network failure. A stalled socket can retain stale authority beyond that target.
Private views
definePrivateView({
name: "availableActions",
shape: availableActionsShape,
dependencies: ["ws/acme/acl", "resource"],
derive: ({ principal, state, policies }) =>
describeActions(principal, state, policies.get("ws/acme/acl")),
validate: (value) => validActions(value),
});
Ordinary clients receive derived permissions, not the ACL. Derivations run server-side. Clients cannot write views. Delivery is a full replacement frame. Empty ready values never stand for unavailability. Denial removes the view; derivation failure makes it unavailable.
The playground reads Session.privateViews.get("availableActions") to enable controls. Applications may select this name in cachePrivateViews and read privateView(name, { allowCached: true }) for stable UI during reconnect. Cached hints are marked cached and contain no subscription or revision; removal and denial clear them. The host still evaluates the submitted operation independently.
Demo policy
src/example/application.ts is one application, not a library invariant:
- Roles:
admin,writer,reader. - ACL readable/writable by admins; readable by authorized services.
- Catalog: members read, admins write.
- Home domain: only that user.
- Content: members read; writers and admins write; authors match the bound user.
- Prefix grants can give a reader write on matching future channels.
- Policy edits cannot remove the last admin.
- New workspaces start with Ada as admin and Beau as writer.
YTTRIUM_DEMO_AUTH=true enables ?as= / workspace=. Same-origin only. The live Worker currently has this on.
Limits
- 16 policy dependencies per content domain.
- 1 MiB incoming text frame; 64 KiB serialized private value; 1 KiB verified-session context.
- 128 UTF-8 bytes for client and subscription ids.
- Ten-second timeout for initial policy synchronization after the policy socket opens.
Turnkey identity-provider adapters, production login UX, strict staleness modes, and policy hydration remain application or follow-up work.