- The table is in
public, RLS is enabled (ALTER TABLE ... ENABLE ROW LEVEL SECURITY), and you want to add policies on top. - You’re signing in users through GoTrue, so
auth.uid()returns the user’s id. - You want to expose the table directly to clients via PostgREST. (If you’re only ever hitting it from a backend with the Service Role key, you don’t need any of this;
service_rolebypasses RLS.)
Step zero: enable RLS, then add policies.
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY; flips the default to “deny all.” If you then add no policies, the table is unreadable by anyone except service_role. Always pair the ENABLE with the policies in a single migration.Pattern 1: Each user sees and edits only their own rows
The most common pattern. Atodos table where every row has an owner_id column, and users can only touch their own rows.
USING decides which rows the policy applies to before the update; WITH CHECK validates the result. Without WITH CHECK, a user could change owner_id from their own id to anyone else’s mid-update and the policy would still pass. Always pair USING + WITH CHECK on UPDATE policies.
Variant: let the server set owner_id. Instead of trusting the client to send owner_id, default it from the session:
INSERT requests that omit owner_id get the caller’s id automatically, and your WITH CHECK (owner_id = auth.uid()) ensures they can’t override it.
Pattern 2: Public read, auth-only write
A blog. Anyone, including unauthenticated visitors, can read posts. Only the author can create, edit, or delete their own posts.posts_public_read and posts_author_read_drafts policies apply when a signed-in user reads, so they see all published posts AND their own drafts. That’s the desired behavior here.
Pattern 3: Tenant isolation (multi-org SaaS)
You’re building a SaaS where each user belongs to one or more organizations and rows are scoped per organization. Adocuments table where users only see documents in orgs they’re a member of.
You need a members table that says who belongs to which org:
documents table policy uses a subquery against members:
IN (SELECT ...) runs once per row scanned at worst. Add an index on members(user_id, org_id) and Postgres will turn it into a hash semi-join. If documents gets large, also index documents(org_id). For very high cardinality, denormalize: stuff the user’s allowed org_ids into the JWT (via a GoTrue hook) and read them from auth.jwt() -> 'org_ids' directly, which avoids the join entirely.
Pattern 4: Role-based access (admin / member)
Extending the tenant pattern: only org admins can delete documents, members can read and create. Two reasonable approaches. Option A: encode role in the policy expression. Re-use themembers.role column:
{"role": "admin"} claim and check it directly:
Pattern 5: Soft delete
Instead of physically deleting rows, you mark them withdeleted_at. Active queries should hide them; admins should still see them. The trick is to filter deleted_at IS NULL in the policy itself, so callers never have to add it.
PATCH /rest/v1/posts?id=eq.{id} with {"deleted_at": "<now>"} instead of DELETE. The policies hide the row from subsequent reads.
Variant: let admins see deleted rows. Add a separate policy for the admin role:
Patterns worth knowing about
A few that come up but don’t need full recipes:-
Force RLS for the table owner. By default the table owner (
service_roleand the project Postgres user) bypasses RLS. To make RLS apply even to the owner (useful for safety in shared environments), useALTER TABLE ... FORCE ROW LEVEL SECURITY. Don’t use this on theai.*tables; the platform’s backend assumes service-role bypass. -
Permissive vs restrictive policies. All policies are
PERMISSIVEby default, so they OR together.RESTRICTIVEpolicies AND together with the result. Useful when you want to layer a “no row may be deleted on Sundays” check on top of existing permissive policies without rewriting them. -
Functions in policy expressions. Postgres caches policy expression results per row per query. A
SELECT 1 FROM members WHERE ...subquery is fine; an HTTP call from inside a policy (viapg_net) is not, and it’ll run thousands of times. Keep policy expressions cheap and deterministic.
Next steps
RLS Testing
Test policies in the SQL Editor or psql before deploying.
RLS Model
How JWTs, roles, and auth.uid() compose under the hood.
Querying the ai schema
The default RLS posture on ai.* and when to tighten it.
ai schema recipes
PostgREST patterns for analytics, bulk ops, and embeds.