Row rules
A row rule limits a table to the rows that belong to the caller. Each rule says: for this table and this operation, only rows where this column equals this claim from the caller’s access token. The data API and realtime both enforce the same rules, so a row a user cannot read through GET /v1/data/... is not sent to them as a realtime event either.
Closed by default
Section titled “Closed by default”A request with no signed-in user (a publishable key and no user access token) is denied on every table unless that table has a public rule for the operation. Reads return an empty list, writes return 403, and realtime sends no events.
Signed-in users are limited by the table’s claim rules. A table with no rules at all is open to every signed-in user, for reading and writing: any user of your app can read, change and delete every row. Give every table that holds user data a rule.
The users table that user table sync creates comes with a rule: each signed-in user sees only their own row.
An example
Section titled “An example”A posts table has an author_id column holding the author’s user id. A rule with:
| Field | Value |
|---|---|
| Table | posts |
| Operation | ALL |
| Column | author_id |
| Claim | sub |
makes every read, insert, update and delete on posts apply author_id = <the user's id>, because sub in a project user’s access token is their user id.
The server fills the owner column
Section titled “The server fills the owner column”For a claim rule that covers inserts (operation INSERT or ALL), the server sets the rule’s column from the signed-in user on every insert and upsert: with the rule above, a new post’s author_id is the caller’s sub. Leave the column out of the request; a row that names someone else is refused. An update (PATCH) cannot change the owner column. vaif generate knows this and makes the column optional in the table’s Insert type (see Types for your tables).
Create a rule
Section titled “Create a rule”Rules are managed through the API, or written in a schema file (see Rules from a schema file); the console has no screen for them yet. Call it as a project developer, with your console access token:
curl -X POST https://api.vaif.studio/rls/policies/$PROJECT_ID \ -H "Authorization: Bearer $VAIF_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tableName": "posts", "name": "Authors own their posts", "operation": "ALL", "field": "author_id", "matchClaim": "sub" }'| Field | Meaning |
|---|---|
tableName |
The table the rule applies to |
operation |
SELECT, INSERT, UPDATE, DELETE or ALL. Reads (list, get, search, aggregates, realtime) use SELECT |
access |
Optional. claim (the default), public (see Public tables), server_only (on ALL) or signed_in_read (on SELECT); see Server-only and shared tables |
field |
The column compared. Required for a claim rule; leave it out of every other kind |
matchClaim |
The access-token claim it must equal, such as sub (user id) or email. Required for a claim rule; leave it out of every other kind |
enabled |
Optional; defaults to on. Turn a rule off without deleting it with POST /rls/policies/:policyId/toggle |
List a project’s rules with GET /rls/policies/:projectId; change one with PATCH /rls/policies/:policyId; remove one with DELETE /rls/policies/:policyId.
Several rules on the same table and operation are combined with AND.
Public tables
Section titled “Public tables”Some tables are meant to be read by visitors who are not signed in, such as published blog posts or a product catalog. Give such a table a public rule:
curl -X POST https://api.vaif.studio/rls/policies/$PROJECT_ID \ -H "Authorization: Bearer $VAIF_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "tableName": "posts", "name": "Anyone can read posts", "operation": "SELECT", "access": "public" }'A public rule lets requests with no signed-in user perform that operation on every row of the table. Use SELECT for public reads; INSERT or ALL also opens writes to anyone who has your publishable key, so use them only for a table such as a contact form’s. Public rules do not change what signed-in users see: they stay limited by the table’s claim rules.
Server-only and shared tables
Section titled “Server-only and shared tables”A table with no rule is open to every signed-in user, for reading and writing. Two more kinds cover tables that are not owned row by row:
- Server-only (
"access": "server_only", operationALL;-- vaif:server-onlyin a schema file): only your server, with the secret key, reads or changes the table. No request with a publishable key may, signed in or not, through the data API or realtime. Use it for tables your server maintains, such as team memberships, audit logs and billing records: a user must never be able to add themselves to a team. - Signed-in read (
"access": "signed_in_read", operationSELECT): every signed-in user reads every row, on purpose.-- vaif:owner-writes <column>in a schema file adds it with claim rules onINSERT,UPDATEandDELETE: every signed-in user reads the table, each changes only their own rows, and the owner column is filled in from their sign-in. Use it for shared feeds and chat. It opens nothing to requests with no signed-in user.
The security advisor counts both as covered and names the tables that use them: a server-only table as one only your server reads or changes, and a signed-in read table as one every signed-in user can read, on purpose.
Rules from a schema file
Section titled “Rules from a schema file”A SQL file applied with vaif db push, or with Apply to Schema in Verament, can create the common rules for you. Put the directive on the line directly above the CREATE TABLE:
-- vaif:owner owner_idCREATE TABLE notes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), owner_id uuid NOT NULL, body text);
-- vaif:public-read-- vaif:owner author_idCREATE TABLE posts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), author_id uuid NOT NULL, title text NOT NULL);-- vaif:owner <column>writes a claim rule<column> = subforALL: each signed-in user reads, updates and deletes only their own rows, and inserts are stamped with their id. The column must beuuidortext.-- vaif:public-readwrites one publicSELECTrule.- With both, the owner rule covers
INSERT,UPDATEandDELETEonly, so everyone can read the table and only the author can change a row. -- vaif:server-onlywrites one server-only rule onALL: only your server, with the secret key, reads or changes the table. It can’t be combined with another directive.-- vaif:owner-writes <column>writes a signed-in read rule plus claim rules<column> = subonINSERT,UPDATEandDELETE, with inserts stamped like-- vaif:owner: every signed-in user reads the table, and each changes only their own rows. It can’t be combined with-- vaif:owner.
-- vaif:server-onlyCREATE TABLE team_members ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), team_id uuid NOT NULL, user_id uuid NOT NULL, role text NOT NULL);
-- vaif:owner-writes author_idCREATE TABLE messages ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), author_id uuid NOT NULL, body text NOT NULL);All are additive: a table that already has rules of that kind keeps them, and nothing is ever removed.
Send the user’s token
Section titled “Send the user’s token”Rules match against the access token the request carries, so create the client with the signed-in user’s token:
import { createVaifClient } from "@vaifstudio/client";
let accessToken = ""; // set after sign-in
const vaif = createVaifClient({ baseUrl: "https://api.vaif.studio", projectId: process.env.VAIF_PROJECT_ID, apiKey: process.env.VAIF_PUBLISHABLE_KEY, accessToken: () => accessToken,});
// With the rule above, this returns only the signed-in user's posts.const { data: mine } = await vaif.from("posts").list();console.log(mine);Only tokens VAIF issued are accepted. A token with a bad signature, or one that has expired, is treated as no token.
A request with no user token has no claims to match, so it is denied unless the table has a public rule (see Closed by default). A signed-in user whose token lacks the claim a rule needs is denied too, instead of seeing the whole table.
Secret keys bypass rules
Section titled “Secret keys bypass rules”Requests made with a secret (vk_srv_…) key are not subject to row rules, and have no signed-in user, so nothing fills an owner column for them: send it yourself. Use a secret key only on servers you control, never in a browser or app bundle; the API refuses secret keys from browser origins. See Key types.