Skip to main content
A policy is a boolean expression evaluated on every row access. It lives beside your entities.

What the expressions see

Roles are checked with the auth.hasRole("x") / auth.hasAnyRole("a", "b") functions (below). There is no auth.roles array and no auth.email binding in policy expressions. (Email/roles live on the session and the SSR page auth prop, just not in the policy evaluator.)

Each action

  • allowRead — runs on query results. If false, row is filtered out.
  • allowInsert — runs against the proposed data (the incoming row).
  • allowUpdate — runs twice, and both must pass. First on the stored row, so a caller can’t edit a row it doesn’t own. Then on the row after the write (the stored row with the patched fields applied), so a caller can’t move a row out of its scope. With auth.tenantId == data.orgId, a member can edit their org’s rows, but a patch of { orgId: "<other org>" } is denied. data and existing both name the row being checked in each pass. This is the same as a Postgres RLS USING clause that also serves as WITH CHECK. It applies to every client write: PATCH /api/entities, /api/sync/push, /api/link, /api/unlink, and CRDT pushes. A state change the rule forbids for the new row (for example data.status == "draft" and an edit that publishes) is denied; make that change in a server function.
  • allowDelete — runs against the stored row (data / existing).
If a policy is omitted, that action is denied by default.

Operators

The policy language is deliberately tiny. The complete operator set is:
Plus these built-in forms:
Ordering (< <= > >=) compares numbers numerically, two strings chronologically when both are valid ISO-8601 timestamps (else lexicographically), and is deny-safe otherwise: comparing null, a boolean, or a number against a non-numeric string is always false. This is what makes data.publishAt <= now deny rows that aren’t published yet. There is still no in, no ends_with/starts_with, and no arithmetic (+ - * /). Membership is expressed with exists(...), not in. String prefix/suffix matching belongs in a function.

Examples

Public read, author-only write

Members of an org

Membership lives in a join entity (OrgMember { orgId, userId }) and is checked with exists(...):
Adding or removing a membership changes who may read the documents without changing the documents. When a synced entity’s read policy or sync scope uses exists(Source where ... field == auth.userId), a change to a Source row tells the user that row names (before and after the change) to re-sync that entity: their open pages get the rows they may now read and drop the rows they may not. Three limits apply:
  • The user is told only if they may read the Source row, so a private row never reveals itself.
  • An update is told only when it changes a field the exists() conditions name.
  • An exists() with no == auth.userId condition names no user.
In those cases the change reaches open pages at the next reconnect or tab focus.

Admin override

Time window (scheduled publishing)

now plus an ordering comparison gates rows by time — public once published, with the author still able to preview:

Rolling time window

ago(...) compares against a moving cutoff instead of a stored timestamp. As a read policy it hides old rows everywhere; more commonly it belongs in a sync scope (sync: { where: ... } on the entity), where it bounds the client replica of an append-only table — recent rows stay live, old rows stay reachable through direct reads and queries:

Policy composition

Multiple policies on the same entity AND together — all must pass. Use this to layer a broad rule with a narrow exception:

Server functions bypass policies

Policies guard the raw /api/entities/* endpoints. Server functions (ctx.db.insert, ctx.db.update, etc.) run with elevated access — they trust you to enforce your own checks inside the handler. This lets you write escape-hatch operations (admin tools, batch imports) without contorting your policy language.

Next

Functions

Write server logic for anything policies can’t express.

Live queries

Policies also filter subscription results.