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.
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.rolesSyntax
| Part | Written as | Notes |
|---|---|---|
| Rule | ALLOW IF … or DENY IF … | One rule per policy. |
| Comparisons | == != > < >= <= | Strings, whole numbers and True / False. |
| Membership | value IN list · list CONTAINS value | "STAFF" IN ctx.actor.roles; ctx.action IN ["INSERT", "UPDATE"]. |
| Range | x BETWEEN low AND high | Inclusive. |
| Logic | AND, OR, NOT, parentheses | AND binds tighter than OR; use parentheses when mixing. |
| Values | "text", 42, True, False, ["a", "b"] | Keywords are case-insensitive; values are not. |
| Fields | ctx.… dot paths | See 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
| Field | What it holds |
|---|---|
ctx.action | What 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_name | The service's own name for the action (sql:select, doc:read, …), whichever spelling your rule uses. |
ctx.resource.name | The table, collection, bucket or channel. |
ctx.resource.tables | Every table a SQL statement reads or writes (a join names several). |
ctx.actor.user_id | The signed-in app user's id; empty for visitors and server keys. |
ctx.actor.is_anonymous | True for the anon key and requests without a user. |
ctx.actor.roles | The caller's roles: an app user's app roles (STAFF, ADMIN…), a team member's member role. |
ctx.actor.role | The main role (anonymous when there is none). |
ctx.actor.lane | product for app users and the anon key; console for team members. |
ctx.storage.path, ctx.storage.folder, ctx.storage.in_own_folder | Storage 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_channel | Realtime 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_id | Where 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
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.
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.Check it
The editor validates the syntax as you type. Check evaluates it for a sample request;
sov policy checkdoes the same from a terminal.Simulate before shipping
Simulate access before you ship replays real requests as every user type against your changed rules and shows what would change.
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 2Exact 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
| Mistake | What happens | Fix |
|---|---|---|
ALLOW IF ctx.actor.is_anonymous == False with no resource | Every 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 Staff | Role 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 rows | A DENY refuses the whole request. | Filter rows with row-level security; use DENY for whole actions. |
AND and OR mixed without parentheses | a OR b AND c means a OR (b AND c). | Add parentheses. |