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.
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 != null2 && oldItem.identityId == identity.id;34require update "price": new.price >= 05 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.
A write that touches a list with rules goes through these steps:
$jsonpad-var variables are substituted, so the rules see exactly what would be stored;allow statements — a refusal here is 403 and nothing else runs;require statements — a failure here is 400;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.
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 createnewthe data after it, or null on deleteoldItemthe item's metadata, including identityId: who owns itidentitythe identity making the write, or nulltokenits id and tags, never its secretnowthe server's clock, which a client can't lie aboutThere 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.
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;23allow update "your turn": identity != null4 && 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.
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.
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.
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.
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.
"$jsonpad-var:now" and have a rule insist on it.pattern instead.