# Permissions

URL: https://docs.domainruntime.dev/docs/auth/permissions
Status: Guide
Reviewed: 2026-09-23



<div className="docs-note">
  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.
</div>

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.

```ts title="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 [#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](/docs/auth/service-keys) is an administrator. Rules do not apply to it.

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

## Operations [#operations]

|                  | Checked when                                                          | `data` is                                      |
| ---------------- | --------------------------------------------------------------------- | ---------------------------------------------- |
| `view`           | the object would appear in a query result                             | the object                                     |
| `create`         | a `tx` creates it                                                     | the object **after** the write, links included |
| `update`         | a `tx` changes it                                                     | the object before; `newData` is after          |
| `delete`         | a `tx` deletes it                                                     | the object                                     |
| `link`, `unlink` | a `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 [#what-a-rule-can-use]

|                               |                                                                                                     |
| ----------------------------- | --------------------------------------------------------------------------------------------------- |
| `auth.id`                     | the signed-in user's id; `null` when signed out                                                     |
| `auth.email`                  | the email the token was minted with, if any                                                         |
| `auth.claims`                 | claims your backend put in the [user token](/docs/auth/users) (up to 1 KB), e.g. `auth.claims.role` |
| `data`                        | the object (see the table above)                                                                    |
| `newData`                     | in `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                                                          |
| `ruleParams`                  | parameters the query sent with itself                                                               |

Rules are [CEL](https://cel.dev) expressions that must evaluate to `true`.

## Following links: `data.ref` [#following-links-dataref]

`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:

```ts
"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` [#reusing-parts-of-a-rule-bind]

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

## Hiding fields [#hiding-fields]

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

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

## Defaults [#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:

```ts
$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 [#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 [#next]

* [Actions](/docs/data/actions) — where writes come from.
* [Users](/docs/auth/users) — where `auth.id` and `auth.claims` come from.
