How access is decided
Who a request acts as, the order Sovaria checks access in, what happens when a rule allows, denies or nothing decides, and how Postgres row-level security fits in.
Every request to your project is checked on the server before it touches data. Two things decide it: your access policies (rules in Sovaria's policy language, managed on the Policies page) and, for tables, Postgres row-level security. Nothing in the browser or app can skip either.
Who a request acts as
| Caller | What it sends | Acts as | Row security |
|---|---|---|---|
| Anonymous visitor | The anon key only | ctx.actor.is_anonymous == True; auth.role() is anon, auth.uid() is null | Applies |
| Signed-in app user | The user's access token (with or without the anon key) | ctx.actor.user_id and auth.uid() are the user; auth.role() is authenticated; their app roles are in ctx.actor.roles | Applies |
| Custom API key | apikey: <custom key> from your server | The key's own scopes; auth.role() is anon | Applies |
| Service role key | apikey: <service role key> from your server or CI | Full access to the project; auth.role() is service_role | Bypassed where the policy allows it (see below) |
| Project team member | A console session, or a token from sov login | Their member role in the organization | Bypassed where the policy allows it (see below) |
For the service role key and team sessions Sovaria sets app.rls_bypass to on. Row-level security policies that include current_setting('app.rls_bypass', true) = 'on' — the console's presets do — let them see every row; policies without it filter them like any other caller.
App users and team members are different people: app users sign up to the app you build (Authentication overview); team members belong to your organization and use the console (Organizations, members and roles). An app role such as ADMIN that you give an app user never grants console or management access.
The order rules are checked in
Organization limits
Limits Sovaria staff set on an organization (for example a suspended service) are checked first and can't be overridden by your rules.
DENY rules
Every active
DENYrule is checked. If any matches, the request is refused with403and the reasonPOLICY_DENY. ADENYrule that reads a field the request doesn't carry also refuses (DENY_CONTEXT_MISSING) — a deny is never skipped because of missing information.ALLOW rules
If no
DENYmatched, the firstALLOWrule that matches lets the request through (POLICY_ALLOW). AnALLOWrule that reads a missing field simply doesn't match.Nothing decided
If no rule decided, the platform's built-in defaults are checked, and otherwise the request is refused (
NO_POLICY_DEFINED). For tables with row-level security enabled and forced, Postgres decides instead — see below.Rows
For tables, an allowed request still runs inside Postgres row-level security, so the caller sees and changes only the rows your Postgres policies let them.
Rules live at two levels: rules created in a project apply to that project (and its environments, which can override them — see Environments, staging and deploying to production); a new organization also gets one baseline rule, ALLOW IF True, so its team members can work straight away. That baseline never admits app users or the anon key: they always need a rule of yours.
Tables: access policies and row-level security together
| Access policies say | Row-level security | Result |
|---|---|---|
| DENY matches | any | 403 — the query never runs. |
| ALLOW matches | enabled | The query runs; Postgres returns or changes only the rows its policies allow. |
| ALLOW matches | off | The query runs on every row the statement names. Fine for public catalogues; dangerous for user data. |
| nothing decided | enabled and forced | Postgres decides alone (the Supabase model). |
| nothing decided | off, or enabled but not forced | 403 NO_POLICY_DEFINED. |
Which database role your app's queries run as is described in Row-level security and access — it is why force matters today.
Documents, storage, realtime and functions
| Service | Actions in rules (ctx.action) | Resource (ctx.resource.name) | More in |
|---|---|---|---|
| Tables | SELECT, INSERT, UPDATE, DELETE (or sql:select, sql:insert… if your rule uses that spelling) | The table | Row-level security and access |
| Documents | doc:read (gets, queries, listeners), doc:write, doc:delete | The collection (the full path, users/u1/orders, for nested ones) | Collections in the console: tree, documents, fields and rules |
| Storage | READ, LIST, WRITE, DELETE | The bucket; the object path is ctx.storage.path | Storage |
| Realtime | SUBSCRIBE, PUBLISH | The channel (ctx.realtime.in_own_channel is true for the caller's user:<user id>… channels) | Realtime |
| Functions | A function's own caller setting: public, signed-in or service key only | The function | Functions |
Managing a bucket (creating, making it public, changing limits, deleting) is project administration, not file access: app users need the admin app role for it even if a rule lets them upload files.
See why a request was allowed or refused
- Simulation sends real requests as each user type and shows the rule that decided each one — Simulate access before you ship.
- Policies › Check (or
sov policy check --action sql:select --resource orders) evaluates your rules for one action without running anything. - Users › Test session opens a session as a real user so you can run queries as them.
- A refused request's response says why; the request log shows the same reason with a trace id — Logs and log drains.