Docs

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.

Updated Oct 17, 2026Open in console

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.

no DENY matchedallowedno ALLOW matchedforced RLSWho is callingsession,key ornobodyOrganization limitsset bySovariastaffDENY rulesanymatch →403ALLOW rulesanymatch →allowedNothing decided403, orPostgresRLS onRow-level securitywhichrows(tables)
Each step can stop the request. A DENY always wins over an ALLOW; when nothing decides, the answer is no.

Who a request acts as

CallerWhat it sendsActs asRow security
Anonymous visitorThe anon key onlyctx.actor.is_anonymous == True; auth.role() is anon, auth.uid() is nullApplies
Signed-in app userThe 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.rolesApplies
Custom API keyapikey: <custom key> from your serverThe key's own scopes; auth.role() is anonApplies
Service role keyapikey: <service role key> from your server or CIFull access to the project; auth.role() is service_roleBypassed where the policy allows it (see below)
Project team memberA console session, or a token from sov loginTheir member role in the organizationBypassed 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

  1. 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.

  2. DENY rules

    Every active DENY rule is checked. If any matches, the request is refused with 403 and the reason POLICY_DENY. A DENY rule that reads a field the request doesn't carry also refuses (DENY_CONTEXT_MISSING) — a deny is never skipped because of missing information.

  3. ALLOW rules

    If no DENY matched, the first ALLOW rule that matches lets the request through (POLICY_ALLOW). An ALLOW rule that reads a missing field simply doesn't match.

  4. 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.

  5. 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 sayRow-level securityResult
DENY matchesany403 — the query never runs.
ALLOW matchesenabledThe query runs; Postgres returns or changes only the rows its policies allow.
ALLOW matchesoffThe query runs on every row the statement names. Fine for public catalogues; dangerous for user data.
nothing decidedenabled and forcedPostgres decides alone (the Supabase model).
nothing decidedoff, or enabled but not forced403 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

ServiceActions in rules (ctx.action)Resource (ctx.resource.name)More in
TablesSELECT, INSERT, UPDATE, DELETE (or sql:select, sql:insert… if your rule uses that spelling)The tableRow-level security and access
Documentsdoc:read (gets, queries, listeners), doc:write, doc:deleteThe collection (the full path, users/u1/orders, for nested ones)Collections in the console: tree, documents, fields and rules
StorageREAD, LIST, WRITE, DELETEThe bucket; the object path is ctx.storage.pathStorage
RealtimeSUBSCRIBE, PUBLISHThe channel (ctx.realtime.in_own_channel is true for the caller's user:<user id>… channels)Realtime
FunctionsA function's own caller setting: public, signed-in or service key onlyThe functionFunctions

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.
Something unclear or missing on this page?Tell us