DomainRuntimedocs
Auth and permissions
GuideWhat works todayReviewed 2026-09-23

Permissions

Rules, per entity, deciding which objects each user can see and change.

The rule engine on this page runs in production today. Declaring rules in the domain with withPermissions is planned; today we set an environment's rules for you from the same document.

Actions decide what an operation does. Permissions decide which data a user can touch at all. You need both: a bug in one action, or a new action written in a hurry, cannot leak or overwrite another user's data if the rules forbid it.

src/domain.ts
export default domain("orders")
  .withSchema({
    entities: {
      orders_order: i.entity({ status: i.string().indexed(), total: i.number() }),
    },
    links: {
      orders_orderOwner: {
        forward: { on: "orders_order", has: "one", label: "owner" },
        reverse: { on: "$users", has: "many", label: "orders" },
      },
    },
    rooms: {},
  })
  .withActions({ /* … */ })
  .withPermissions({
    orders_order: {
      bind: { isOwner: "auth.id in data.ref('owner.id')" },
      allow: {
        view: "isOwner",
        create: "isOwner",
        update: "isOwner",
        delete: "false",
      },
    },
    $default: { allow: { $default: "false" } },
  });

Users see only their own orders. An action can create or change an order only for the user who called it. Nobody deletes one. Everything else is denied.

Who is checked

Rules run against the caller of the action, not the action:

  • A signed-in user who calls orders.place creates the order as that user, so the create rule must allow it.
  • The browser cannot write at all: the environment accepts writes only from your action code. You do not need create: "false" to keep browsers out; write rules for the user an action acts for.
  • A service key is an administrator. Rules do not apply to it.

Development environments apply the same rules, so you can test permissions while you develop.

Operations

Checked whendata is
viewthe object would appear in a query resultthe object
createa tx creates itthe object after the write, links included
updatea tx changes itthe object before; newData is after
deletea tx deletes itthe object
link, unlinka tx links or unlinks, per label: allow: { link: { owner: "…" } }the object

A hidden object is simply missing from query results — no error, no count, so the caller cannot tell it exists. A denied write fails the whole tx with permission-denied.

What a rule can use

auth.idthe signed-in user's id; null when signed out
auth.emailthe email the token was minted with, if any
auth.claimsclaims your backend put in the user token (up to 1 KB), e.g. auth.claims.role
datathe object (see the table above)
newDatain update, the object after the change
data.ref('path.attr')values reached through links, always a list
auth.ref('$user.path.attr')the same, starting from the signed-in user
ruleParamsparameters the query sent with itself

Rules are CEL expressions that must evaluate to true.

data.ref walks links and returns a list, even for a has: "one" link. Compare with in, never ==, and end the path with an attribute:

"auth.id in data.ref('owner.id')"        // ✅
"auth.id == data.ref('owner.id')"        // ❌ compares a string with a list
"auth.id in data.ref('owner')"           // ❌ no attribute at the end
"auth.id in newData.ref('owner.id')"     // ❌ newData has no ref; on create, use data

Reusing parts of a rule: bind

orders_order: {
  bind: {
    isOwner: "auth.id in data.ref('owner.id')",
    isManager: "auth.claims.role == 'manager'",
  },
  allow: { view: "isOwner || isManager", update: "isManager" },
},

Hiding fields

To let everyone see an entity but only some users see one attribute, add a fields rule:

orders_customer: {
  allow: { view: "true" },
  fields: { email: "auth.id in data.ref('account.id')" },
},

Defaults

For your entities, a missing rule allows. $default changes that for every entity and operation you did not write a rule for. Start from deny and open what you need:

$default: { allow: { $default: "false" } },

System entities have their own defaults: $files and $streams are denied until you add rules; each user sees only their own $users row and their own $executions.

When a rule says no

  1. Reproduce it with a user token against a production release (development runs actions as an administrator today).
  2. Split the rule. If isOwner && isManager fails, try each half alone.
  3. Check the link. data.ref('owner.id') is empty if the action forgot .link({ owner: … }) in the same tx.

Next

  • Actions — where writes come from.
  • Users — where auth.id and auth.claims come from.

On this page