auth.uid(), auth.jwt(), auth.role()), and the trade-offs between the four credentials Powabase issues per project. For policy patterns, see the RLS Cookbook. For local testing without spinning up a frontend, see RLS Testing.
The four credentials and their roles
The Connect modal hands out five values; four of them are credentials, and each maps to a distinct Postgres role:| Credential | Postgres role | Pre-issued JWT? | Typical use |
|---|---|---|---|
| Anon (Publishable) Key | anon | Yes (signed aud=authenticated, role=anon) | Embedded in clients; sets the floor for “what unauthenticated visitors see” |
| Signed-in user access token | authenticated | Issued by GoTrue on sign-in | What your app gets back after POST /auth/v1/token |
| Service Role (Secret) Key | service_role | Yes (signed role=service_role) | Server-side; bypasses RLS |
| Database URL (direct Postgres) | <ref> (project owner) | n/a | Migrations, admin scripts; not RLS-checked |
role claim is hard-coded to anon and service_role respectively. PostgREST reads the role claim and sets the session’s database role to match — SET LOCAL ROLE anon or SET LOCAL ROLE service_role.
A signed-in user’s token has role=authenticated. PostgREST sets the role to authenticated, and your RLS policies that target TO authenticated apply.
The auth helper functions
Once PostgREST has set the role and stored the JWT claims as a session-local setting (request.jwt.claims), three SQL functions in the auth schema give policies access to the user’s identity:
USING and WITH CHECK policy expressions:
auth.uid() returns NULL for anon requests (they have no sub). That means an anon-targeting policy can check WHERE owner = auth.uid() and it’ll just never match — safe by default.
Custom claims you set in JWTs (via GoTrue hooks or your own minting) land in auth.jwt():
The defaults
When a new project is provisioned,public ships empty (you bring your own tables) and ai ships with a comprehensive default policy set.
public schema — empty by default. RLS is enabled on no tables until you create some. When you create a table in public, RLS is off by default — you must ALTER TABLE ... ENABLE ROW LEVEL SECURITY and add policies, otherwise PostgREST will refuse the request (it requires either RLS-enabled with policies, or grants — see the Cookbook).
ai schema — RLS enabled on every table. Default policies are:
service_rolehas full access (FOR ALL USING (true) WITH CHECK (true)) on every table. This is what the platform’s own backend uses.authenticatedhas read access on every table. Most config tables (agents,workflows,tools,knowledge_bases,sources) are also writable. Session-shaped tables (agent_sessions,agent_runs,orchestration_sessions,orchestration_runs) read with per-user filtering viaauth.uid() = user_id OR user_id IS NULL.anonhas no policies. Anon-key requests againstai.*return empty.
Client-side vs server-side: when each role is right
Service Role and Anon aren’t “tiers of privilege” — they’re for different settings entirely. Use the Anon Key client-side. Embed it in browser JS, mobile apps, anything that ships to user devices. RLS is what makes this safe: even though the key is public, the policies you write decide whatanon (and authenticated, after sign-in) can do. The key’s value is its role claim — possessing it doesn’t grant access; the policies do.
Use the Service Role Key server-side only. It bypasses RLS entirely (service_role has BYPASSRLS set on the database role). Treat it like a database password — never ship it in client code, never embed it in a Authorization header you trust to a third party. It’s for your backend server-to-server calls, batch jobs, migrations, the typed /api/* surface, and anything else inside your trust boundary.
Use signed-in user tokens for end-user identity. When a user signs in via POST /auth/v1/token?grant_type=password, GoTrue returns an access_token and refresh_token. Send the access token as Authorization: Bearer <token> on every subsequent request. PostgREST will set the role to authenticated and your auth.uid() lookups will return that user’s id.
Use the Database URL only from trusted, server-side environments. It’s a <ref> Postgres user with full schema ownership — RLS doesn’t even apply (the role has BYPASSRLS). Use it for migrations, BI tools, and admin scripts; never for application traffic.
Composition with the typed /api/* surface
The typed AI endpoints (/api/agents, /api/sessions, etc.) authenticate with the Service Role key and do their own ownership checks at the application layer. That means the RLS policies on ai.* don’t directly affect those endpoints — the backend is already running with full access.
What RLS does affect is what your end users see when they query ai.* directly via PostgREST. If you’re not exposing ai.* to end users (you only hit /api/* from your backend), the defaults are fine; nobody on a user JWT ever touches it. If you are exposing ai.* reads to end users (custom dashboards, real-time subscriptions), the defaults’ “any authenticated user sees everything” posture is the thing to be careful about.
Next steps
RLS Cookbook
Five patterns: own-rows-only, public-read+auth-write, tenant isolation, role-based, soft-delete.
RLS Testing
Test policies in psql or the SQL Editor without a frontend.
Auth & Connection
Where the four credentials come from in the Studio.
Querying the ai schema
The companion page for how RLS interacts with the AI-surface tables.