Write rules

Write rules decide what your API tokens are allowed to write. They're a few lines of text on a list, and the API checks them on every create, update and delete, before anything is saved.

Token permissions answer "may this token update items in this list?". Rules answer the questions that depend on the data: may this player move when it isn't their turn, may a paid order be edited, may this field ever change once it's set. Without them, anything a client is allowed to write, it's allowed to write anything to.

That's what lets a browser or a game client talk to JSONPad directly. The rules are enforced on our side, so a client that's been tampered with gets the same answer as an honest one.

JSONPad doesn't run your code. Rules are a small, deliberately limited language that can only say yes or no: they can't loop forever, call out to the internet, or change the data. When you need to do something when data changes, that's what webhooks are for.

Authorise, then validate

There are two kinds of rule, and they do different jobs.

  • allowWho may write. If an operation has any allow statements, at least one of them must be true or the write is refused with 403. The message is always the same, on purpose: a refused client learns nothing about your rules.
  • requireWhat they may write. Every require that covers the operation must be true, or the write fails with 400 and the message you wrote after else, which is meant to be read by whoever is using your app.
1allow update, delete: identity != null
2 && oldItem.identityId == identity.id;
3
4require update "price": new.price >= 0
5 else "price must not be negative";

An operation with no allow statement is left to token permissions, and its require statements still run. That's a useful combination: anyone your token lets in may write, but only data that passes your checks.

The order of a write

A write that touches a list with rules goes through these steps:

  1. the token's permissions, as always;
  2. $jsonpad-var variables are substituted, so the rules see exactly what would be stored;
  3. the allow statements — a refusal here is 403 and nothing else runs;
  4. the list's JSON schema;
  5. the require statements — a failure here is 400;
  6. the item is re-read and locked, and the write is saved (see consistency).

Schema first, then require, means your rules can trust the shape of the data: if the schema says price is a number, new.price >= 0 won't be comparing a string. Use the schema for shapes and types, and rules for everything that depends on the old data or on more than one field at once.

Rules apply to writes made with an API token. They don't apply to your own writes in the dashboard or in user auth mode, which is how you always keep a way in.

What a rule can see

A rule is one expression. It can read the data before and after the write, the item's metadata, the identity and token making it, the list, and the server's clock:

  • oldthe item's data before the write, or null on create
  • newthe data after it, or null on delete
  • oldItemthe item's metadata, including identityId: who owns it
  • identitythe identity making the write, or null
  • tokenits id and tags, never its secret
  • nowthe server's clock, which a client can't lie about

There are around forty built-in functions for working with those values, including unchanged and onlyChanged, which compare old and new by JSON pointer. The language reference lists all of them, and the recipes show the patterns most apps need.

Shared items

Normally an identity may only write items it owns, so two people can never touch the same item. A game, a shared document or a chat room needs the opposite. A rule set can say so:

1shared update;
2
3allow update "your turn": identity != null
4 && identity.id == old.currentPlayerId;

shared update; (or shared update, delete;) lifts the ownership check for that operation, and the rules take over deciding who may write. It only applies when the token holds the matching update-with-identity or delete-with-identity permission, and oldItem.identityId still tells you who the item really belongs to.

Consistency

Rules are checked against the item as it is at that moment. To make sure that's still true when the write lands, the item is re-read and locked inside the same transaction. If another write changed it in between, the rules were checked against a version that no longer exists, so nothing is saved and you get 409 ITEM_CONFLICT. Fetch the item again and retry.

That's what makes rules like "this counter can only go up by one" safe: two clients that race both read the same old value, and only one of them lands.

You can also make a write conditional yourself, with the If-Match header and an item's ETag. If the item has changed since you fetched it, the write is refused with 412 before the rules even run. This works on every list, with or without rules.

Writing and testing rules

Open a list in the dashboard and click Edit rules. The editor highlights the language, checks it as you type, and suggests field names from your list's schema. Beside it, a playground runs a candidate write and shows you every part of every rule and what it evaluated to, so a refusal is never a mystery.

Rule tests are saved with the rules. Each one is a write and what you expect to happen to it. They run in the dashboard while you type, in the command line tool offline, and on the API every time the rules are saved: rules whose tests fail are refused, so you can't ship a change that breaks something you'd already checked.

Rules can also live in your repository and be deployed with schema sync, as rulesFile and rulesTestsFile next to the rest of your list configuration.

POST /lists/:list/rules/test is the same check over HTTP: it tells you what the rules would do with a write, without making it.

Refused writes

The rules page lists the last 50 writes your rules refused: what was attempted, which rule refused it and when. There's no data and no token secret in the log. It's the first place to look when a client says something doesn't work, and it's worth glancing at after changing a rule set.

Limits

Rules have to finish, quickly, on every write, so the language has no loops and no recursion, and evaluation has a budget. The limits are the same on every plan, and are listed under limits and quotas. Ordinary rules use a tiny fraction of the budget — the playground shows you how much.

What rules can't do

  • Read other items or lists. A rule only sees the item being written. Checks like "this order's customer must exist" belong on your server for now.
  • Change the data. Rules accept or refuse a write; they never rewrite it. To have the server fill in a value, write "$jsonpad-var:now" and have a rule insist on it.
  • Govern reads. Reading is controlled by token permissions, identities and guard indexes.
  • Apply to lists, indexes or identities. Rules are about items.
  • Use regular expressions. Use the list's JSON schema pattern instead.
  • Run on a schedule. Nothing happens until a request arrives. A deadline is expressed as a rule that lets any client fire it once it's due — see the recipe.