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:
The Anon Key and Service Role Key are pre-issued long-lived JWTs that the platform mints when it provisions your project. They’re shaped exactly like a user access token, but their
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 is locked to the backend.
public schema, empty by default. When you create a table in public, RLS is off, and the project’s Postgres image grants anon and authenticated full privileges on new public tables by default (ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ... anon, authenticated, service_role in the Postgres image’s initial schema). PostgREST doesn’t refuse such a table: until you enable RLS, anyone holding the Anon key can read and write every row through /rest/v1/*. Enable RLS as part of creating every table, then add policies:
REVOKE ALL ON public.my_table FROM anon, authenticated.
ai schema, backend-only. Since project-service migration 0025, anon and authenticated have no grants and no policies on ai.*, and ai isn’t in PostgREST’s exposed schemas. RLS stays enabled with no client policies, so access is deny-by-default; the project service reaches it with the service role, which bypasses RLS. There’s nothing to tighten here: end users can’t query ai.* directly under any key. See The ai schema.
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, and 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
RLS doesn’t govern the typed AI endpoints (/api/agents, /api/knowledge-bases, and so on); the project service runs their queries against ai.* itself. Most of them require the Service Role key. A signed-in user’s JWT works only on the conversation routes, where application code scopes everything to the user’s own sessions and runs: another user’s session returns 404, and a run won’t continue a session that isn’t the caller’s. See Which key for which route.
RLS does come back into play inside an agent run. When a user’s own JWT starts the run, the agent’s database_query and database_write tools query Postgres as the authenticated role with that user’s claims, so your policies on the agent’s configured tables apply to what the agent reads and writes. When your backend starts the run with the Service Role key, the tools use the agent’s own database login, which bypasses RLS on its configured tables. See What agent data tools act as.
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.
The ai schema
Why ai.* is backend-only and how to reach AI resources.