---
title: Rules
description: The rules language, what a rule can name, and how rules on collections and fields are enforced.
section: Data
order: 3
---

# Rules

<p class="lead">A rule is a filter expression that can reference the caller. It's compiled into the SQL that reads or writes the rows, so rows a user can't see never leave SQLite.</p>

## Where rules go

Each collection has five rules, one per action. A missing rule means superusers only; `""` means anyone.

```json title="migrations/V1__init.json"
"rules": {
  "list":   "@request.auth.id != null",
  "view":   "@request.auth.id != null",
  "create": "author = @request.auth.id",
  "update": "author = @request.auth.id",
  "delete": "author = @request.auth.id || @request.auth.staff = true"
}
```

Fields can have two rules of their own: `visible` (who can read it) and `writable` (who can set it). Both default to the collection's rules.

```json title="migrations/V1__init.json"
{ "name": "mark", "type": "number",
  "visible":  "@request.auth.id = pupil || @request.auth.staff = true",
  "writable": "@request.auth.roles ~ \"examiner\"" }
```

## The language

Same syntax as [filters](/docs/api#filters):

| | |
|---|---|
| Compare | `=` `!=` `>` `>=` `<` `<=` |
| Contains / doesn't contain | `~` `!~` |
| Combine | `&&` `\|\|` and parentheses |
| Values | `"text"`, numbers, `true`, `false`, `null` |
| The record | field names: `author`, `org`, `status` |
| The caller | `@request.auth.…` |
| Time | `@now`, `@days_ago.30`, `@years_ago.13` |

## The caller

- `@request.auth.id`, `collection`, `email` and `superuser` come from the token and are free.
- Any other field of the caller's record works too: `@request.auth.staff`, `@request.auth.org`. The record is loaded by id, in the same transaction, only when a rule uses it.
- `@request.auth.roles` is a set: `@request.auth.roles ~ "admin"` tests membership, so it never matches "superadmin".
- For anonymous callers all of these are `null`, so a members-only rule just evaluates to false, never an error.

```text title="rule"
@request.auth.staff = true && org = @request.auth.org && id != @request.auth.id
```

Staff can delete users in their own organisation, but not themselves.

## How they are enforced

- **Lists filter.** Callers get only rows they may see, with correct totals.
- **View, update and delete return 404** when denied, so rules can't be used to probe whether a row exists.
- **Writes are checked twice:** before the change and again inside the transaction after it. A change that would put the row out of the writer's own reach is rolled back.
- **Caller-only conditions are resolved before the query.** `@request.auth.roles ~ "admin"` becomes a constant; conditions on the record go into the `WHERE` clause.
- **Field rules** work the same way: a field the caller can never see isn't selected; one that depends on the row is nulled per row, in SQL.

## Checking permissions up front

`GET /api/acl` returns, for the signed-in user, what each collection allows: `allow`, `deny`, or `conditional` (only for some rows, e.g. their own). Use it to show only buttons that will work. It evaluates the same rules, so it can't disagree with enforcement.
