Rules & Visibility
Who can see and who can change each part of a realm: path patterns, owner checks, serverChecked conditions, and how hidden state stays on the server.
Rules answer two questions for every path in a realm’s state: who may see it (view) and who may change it directly (alter). They live in the shared definition, so they run on the client as prediction and on the server as authority.
rules: (r) => [ // Everyone sees config; only server code writes it. r.path("config.*").view(r.everyone).alter(r.serverOnly),
// Presence: everyone sees all cursors; you may only move your own. r.path("presence.$avatarId.{cursorX,cursorY}") .view(r.everyone) .alter(({ avatar, params }) => avatar.persistentId === params.avatarId || err_market.fromId("not_yours")),
// Vendor fast lane: a vendor sets their own prices directly, // with a server-checked floor. r.path("inventory.$vendorId.$produceId.pricePerKg") .view(r.everyone) .alter(r.all( ({ avatar, params }) => avatar.persistentId === params.vendorId || err_market.fromId("not_your_stall"), r.serverChecked(({ next, ctx }) => next >= ctx.priceFloor || err_market.fromId("below_floor")), )),],Path patterns
Section titled “Path patterns”A pattern addresses a set of leaves in the state shape. Patterns are resolved against the schema at the type level, so rule functions get precise params, prev and next types.
| Pattern piece | Matches |
|---|---|
config.open | One exact leaf. |
config.* | Every child under config. |
presence.$avatarId | Each entry of a t.record — binds the key as params.avatarId. |
players.$playerId.{x,y} | A brace set: just x and y under each player. |
When several rules could match a leaf, the most specific pattern wins. Anything no rule matches is default-deny, for both viewing and writing, so you opt paths in.
Containers are structure, not data. A record or object reached through fixed keys, such as simulator.conditions, is always in the projection (as {} when empty) once any rule reaches beneath it, so a rule on its entries (simulator.conditions.$ref) is enough for a client to write the first entry. Inside a record entry the entry’s own rules decide, because an entry’s existence is data.
Wildcards (*, **) need at least one further segment, so they never match the node they hang off. A wire-leaf (a primitive, a t.array, a t.schema) is therefore addressed by its exact path: r.path("terrain"), not r.path("terrain.**"). defineRealm throws on patterns that could never match a schema path, such as a typo’d key or a wildcard descending into a wire-leaf. A dead rule then fails at definition time instead of silently denying its branch.
View conditions
Section titled “View conditions”view decides which avatars receive a path’s values and its changes. View conditions are coordinate-only: they see { avatar, params } and never state, because the server evaluates them per leaf each time it slices a broadcast.
r.everyone— all avatar types (including replica-class avatars).r.avatarTypes("vendor", "auditor")— listed types only.- A plain function
({ avatar, params }) => boolean— e.g. owner-only:avatar.persistentId === params.avatarId.
A view rule’s avatar has type and persistentId only, never instanceId. Reading it won’t compile. Broadcasts are sliced and cached per view signature (type, persistentId), so one avatar’s tabs and devices share one encoded payload, and a view branching on instanceId would be silently mis-sliced across them. Per-instance visibility is unsupported by design. Alter rules run per write and do see the full identity, including instanceId.
A hidden branch’s bytes never leave the server. Broadcasts are sliced per view signature and encoded once per audience group. Clients don’t “receive and ignore” hidden state; it is never sent to them.
Owner visibility is a state-shaping pattern
Section titled “Owner visibility is a state-shaping pattern”To let each consumer see only their own orders while vendors see the orders for their stall, skip clever conditions and shape the state so the coordinate is in the path, and denormalise per audience inside the intent that writes both:
state: { // keyed by the viewing coordinate — the view rule is then just an owner check ordersByConsumer: t.record(t.id("consumerId"), /* … */), ordersByVendor: t.record(t.id("vendorId"), /* … */),}Alter conditions
Section titled “Alter conditions”alter decides who may directly write a path with realm.update(). Intents bypass alter rules: they’re trusted server code gated by their own allow list.
r.serverOnly— onlyengine.update()(server code) writes here.- A plain function
({ avatar, params, prev, next, state, basisVersion }) => true | NiceError— runs on the client as prediction and on the server as authority. Returntrueto allow, or a NiceError to reject with a typed reason. r.serverChecked(fn)— a check that needs server-only facts: the injectedctx, or the log-backed compare-and-set helpertouchedSince(basisVersion). Client prediction auto-passes it, so this is the one kind of check that can cause a rollback.r.all(...conds)— combines conditions: all must pass, and the first NiceError rejects.
Because plain alter functions run in prediction, a disallowed write rejects locally and instantly. The rejection appears on the settle handle and in onMutationRejected, and nothing is sent.
Compare-and-set with touchedSince
Section titled “Compare-and-set with touchedSince”For the rare direct write that must not clobber a concurrent edit, a serverChecked condition can consult the patch log:
r.serverChecked(({ basisVersion, touchedSince }) => !touchedSince(basisVersion) || err_market.fromId("concurrent_edit"))basisVersion is the confirmed version the writer composed against, and touchedSince asks “was this path modified after that?” If you need this routinely, the mutation probably wants to be an intent instead.
Replicas are an avatar rule
Section titled “Replicas are an avatar rule”A Worker, or any server-side process, holding a live read copy of a realm is another avatar type. Its view rules say “everything” (or a partial slice), and it connects with connectRealmReplica. No special authority, no second code path: the same rules decide what it sees. See Connecting & React.