---
title: Rules
description: The rules language, what a rule can name, and how rules on collections and fields are enforced.
section: Data
order: 10
---

# Rules

<p class="lead">A rule is a filter that names the caller. It is compiled into the SQL that reads or writes the rows, so a row somebody may not see never leaves SQLite.</p>

## Where rules go

Each collection has five, one per action. Leave one out and only superusers may do it; `""` lets anybody.

```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"
}
```

A field can have two of its own: `visible`, who may read it, and `writable`, who may set it. Both default to whoever the collection already let in.

```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

The same one [filters](/docs/api#filters) use:

| | |
|---|---|
| Compare | `=` `!=` `>` `>=` `<` `<=` |
| Contains, or doesn't | `~` `!~` |
| Combine | `&&` `\|\|` and parentheses |
| Values | `"text"`, numbers, `true`, `false`, `null` |
| The record | its field names: `author`, `org`, `status` |
| The caller | `@request.auth.…` |
| Moments | `@now`, `@days_ago.30`, `@years_ago.13` |

## What a rule can say about the caller

- `@request.auth.id`, `collection`, `email` and `superuser` come from the token and cost nothing.
- Any other field of the caller's own record works too: `@request.auth.staff`, `@request.auth.org`. Sluurp reads that record only when a rule asks, by its id, in the same transaction.
- `@request.auth.roles` is a set. `@request.auth.roles ~ "admin"` asks whether "admin" is one of them, so it never matches "superadmin".
- For somebody who isn't signed in, every one of these is `null`. A rule written for members is simply false for them, never an error.

```text title="rule"
@request.auth.staff = true && org = @request.auth.org && id != @request.auth.id
```

Staff may delete people in their own organisation, but not themselves.

## How they are enforced

- **Lists filter.** A caller gets the rows they may see, and the counts are right.
- **View, update and delete answer 404** when the rule says no, so a rule can't be used to find out that a row exists.
- **Writes are checked twice.** The row is checked before the change and again inside the transaction after it. A change that would put a row out of the writer's own reach is rolled back.
- **A rule about the caller alone is settled before the query.** `@request.auth.roles ~ "admin"` becomes true or false and the query never sees it. A rule about the record becomes part of the query's `WHERE`.
- **Field rules too.** A field that nobody in the caller's position may see is not selected. One that depends on the row is blanked row by row, in SQL.

## Asking before trying

`GET /api/acl` answers, for the signed-in caller, what each collection lets them do: `allow`, `deny`, or `conditional` (they may, for some rows, such as their own). A screen uses it to show only the buttons that can work. It is the same rules asked in advance, not a second set, so the two can't disagree.
