Docs

Policy language (SPL)

The syntax of access policies: ALLOW and DENY rules, the ctx fields you can test, operators, precedence, and how to write, check, version and roll back rules.

Updated Oct 17, 2026Open in console

An access policy is one rule: ALLOW IF <condition> or DENY IF <condition>. The condition reads facts about the request — who is calling, what they want to do and to which resource — under ctx. Rules are compiled and checked on the server for every request.

ALLOW IF ctx.resource.name == "products" AND ctx.action == "SELECT"
ALLOW IF ctx.resource.name == "orders" AND ctx.actor.is_anonymous == False
DENY  IF ctx.resource.name == "audit_log" AND ctx.action IN ["UPDATE", "DELETE"]
ALLOW IF "STAFF" IN ctx.actor.roles

Syntax

PartWritten asNotes
RuleALLOW IF … or DENY IF …One rule per policy.
Comparisons== != > < >= <=Strings, whole numbers and True / False.
Membershipvalue IN list · list CONTAINS value"STAFF" IN ctx.actor.roles; ctx.action IN ["INSERT", "UPDATE"].
Rangex BETWEEN low AND highInclusive.
LogicAND, OR, NOT, parenthesesAND binds tighter than OR; use parentheses when mixing.
Values"text", 42, True, False, ["a", "b"]Keywords are case-insensitive; values are not.
Fieldsctx.… dot pathsSee below.

There are no loops, functions or backward jumps: every rule runs in a fixed number of steps, and a rule that can't be evaluated never allows anything.

Fields you can test

FieldWhat it holds
ctx.actionWhat the caller wants to do. Tables: SELECT, INSERT, UPDATE, DELETE. Documents: doc:read, doc:write, doc:delete. Storage: READ, LIST, WRITE, DELETE. Realtime: SUBSCRIBE, PUBLISH.
ctx.action_nameThe service's own name for the action (sql:select, doc:read, …), whichever spelling your rule uses.
ctx.resource.nameThe table, collection, bucket or channel.
ctx.resource.tablesEvery table a SQL statement reads or writes (a join names several).
ctx.actor.user_idThe signed-in app user's id; empty for visitors and server keys.
ctx.actor.is_anonymousTrue for the anon key and requests without a user.
ctx.actor.rolesThe caller's roles: an app user's app roles (STAFF, ADMIN…), a team member's member role.
ctx.actor.roleThe main role (anonymous when there is none).
ctx.actor.laneproduct for app users and the anon key; console for team members.
ctx.storage.path, ctx.storage.folder, ctx.storage.in_own_folderStorage only: the object path, its first folder, and whether that folder is the caller's user id (<user id>/avatar.png).
ctx.realtime.in_own_channelRealtime only: the channel is the caller's private channel, user:<user id> or anything under it (user:<user id>:notifications).
ctx.org_id, ctx.project_idWhere the request is going.

How rules combine

  • A matching DENY always wins, whichever order rules were created in.
  • Otherwise any matching ALLOW lets the request through.
  • If nothing matches, the request is refused (tables with forced row-level security are the exception — see How access is decided).
  • A DENY that tests a field the request doesn't carry refuses; an ALLOW in the same situation doesn't match.

Write, check and ship a rule

  1. Start from a template

    Policies › New policy lists templates (signed-in read, owner only, staff write, own folder, own channel…). Pick one and change the names.

  2. Name it after what it covers

    Prefix the name with [table:orders], [collection:carts] or [bucket:avatars] and the console shows the rule with that table, collection or bucket.

  3. Check it

    The editor validates the syntax as you type. Check evaluates it for a sample request; sov policy check does the same from a terminal.

  4. Simulate before shipping

    Simulate access before you ship replays real requests as every user type against your changed rules and shows what would change.

  5. Save

    Saving applies the rule to the next requests. Every save is a new version.

sov policy templates
sov policy upsert pol_orders_owner --org <project-id> --name "[table:orders] Signed-in read" \
  --spl 'ALLOW IF ctx.resource.name == "orders" AND ctx.action == "SELECT" AND ctx.actor.is_anonymous == False'
sov policy check --action sql:select --resource orders
sov policy history pol_orders_owner
sov policy rollback pol_orders_owner --version 2

Exact flags and methods: CLI: Policies, rules tests, simulation and audit, JavaScript: Policies and audit, Python: Policies and audit, REST: policies, simulation, audit and history.

Versions, environments and rollback

  • Every change to a rule is a numbered version; History shows who changed what, and Roll back restores an earlier version as a new one.
  • Deleting a policy deactivates it; its history stays.
  • An environment (staging, a version) can have its own rules that replace production's while you test; promote them when ready — Environments, staging and deploying to production.
  • Policy changes are recorded in the audit log (Audit log).

Common mistakes

MistakeWhat happensFix
ALLOW IF ctx.actor.is_anonymous == False with no resourceEvery signed-in user may use every table, bucket and channel.Always name the resource: ctx.resource.name == "orders" AND ….
"STAFF" in ctx.actor.roles vs a role named StaffRole values are compared exactly.Use the role names exactly as on the Users page (they are stored in capitals).
Relying on a DENY to hide some rowsA DENY refuses the whole request.Filter rows with row-level security; use DENY for whole actions.
AND and OR mixed without parenthesesa OR b AND c means a OR (b AND c).Add parentheses.
Something unclear or missing on this page?Tell us