Row-based permissions in Supabase are PostgreSQL Row Level Security (RLS) policies that restrict which rows a user can select, insert, update, or delete, based on their identity or role.
Row-based permissions—also called row-level security (RLS)—let you decide which individual rows in a table each user can read or modify. Instead of granting blanket access at the table or column level, you write policies that evaluate per-row predicates at query time. In Supabase, RLS is powered by the native PostgreSQL feature, but Supabase layers helpful defaults, session auth, and dashboard tooling on top.
Nearly every multi-tenant SaaS, analytics platform, or mobile app needs to isolate data between users or organizations. Hard-coding WHERE user_id = :current_user
into every query is error-prone and brittle. RLS policies move that logic into the database where it is:
Supabase stores an authenticated user’s JWT inside a PostgreSQL session variable via set_config('request.jwt.claims', ...)
. Inside your policy expressions you can reference claims such as auth.uid()
or custom metadata. Once you ENABLE ROW LEVEL SECURITY
on a table, PostgreSQL denies all access until at least one policy explicitly grants it.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY
for each command (SELECT, INSERT, UPDATE, DELETE).role
and jwt
headers using psql, Supabase Studio, or a SQL editor like Galaxy.CREATE TABLE projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id uuid REFERENCES organizations(id),
name text NOT NULL,
created_at timestamptz DEFAULT now(),
created_by uuid NOT NULL
);
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Org members can read their projects"
ON projects
FOR SELECT
USING (organization_id IN (
SELECT organization_id
FROM memberships
WHERE user_id = auth.uid()
));
CREATE POLICY "Org members can create projects"
ON projects
FOR INSERT WITH CHECK (
organization_id IN (
SELECT organization_id FROM memberships WHERE user_id = auth.uid())
AND created_by = auth.uid()
);
You might allow only project owners to update or delete:
CREATE POLICY "Only creator can modify project"
ON projects
FOR UPDATE USING (created_by = auth.uid())
WITH CHECK (created_by = auth.uid());
CREATE POLICY "Only creator can delete project"
ON projects
FOR DELETE USING (created_by = auth.uid());
Start with zero access: once ENABLE ROW LEVEL SECURITY
is on, no one—including service_key users—can see rows unless a policy explicitly returns TRUE
. Add precise policies one command at a time.
Create a PostgreSQL role (e.g., db_admin
) that is BYPASSRLS
to run migrations, backfills, or analytics without policy interference.
While legal, deeply nested sub-queries in policy predicates can hurt performance. Instead, pre-materialize membership relationships in join tables with proper indexes.
Store all CREATE POLICY
and ALTER TABLE ... RLS
statements in migration files so staging and production stay identical.
Write unit tests that connect as different roles and assert allowed/denied behavior. Tools like pgTAP
or orval
help automate this.
Because Galaxy is a modern SQL IDE with role switching and AI assistance, you can:
SET ROLE end_user;
) to validate RLS policies without leaving the editor.CREATE POLICY
statements with correct predicates based on your schema.RLS enforcement happens in Postgres, so all queries executed from Galaxy respect your policies automatically.
Imagine you run a bug-tracking SaaS where each customer (organization) must see only their tickets. The minimal set of policies:
ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Org can read their tickets" ON tickets
FOR SELECT USING (organization_id = auth.uid()::uuid);
CREATE POLICY "Org can file tickets" ON tickets
FOR INSERT WITH CHECK (organization_id = auth.uid()::uuid);
Because the policy predicate uses the implicit claim auth.uid()
, your API, Realtime, and Edge Functions all inherit the same protections.
Policy predicates are compiled into the query plan. With proper indexes they incur negligible overhead.
RLS is enforced at the database layer; psql, SQL editors, GraphQL, and REST all pass through the same gate.
service_role
key is safer."The service_role
bypasses RLS entirely—great for server-side jobs, dangerous for client apps. Prefer anon
or custom JWTs.
pg_row_security
view to see which policy blocked you.auth.uid()
returns the expected UUID in the session (select auth.uid();
).SET ROLE db_admin;
or disable RLS in a transaction, then re-enable.Row-based permissions in Supabase Postgres leverage PostgreSQL’s battle-tested RLS feature to deliver data isolation that is safe, performant, and centrally managed. By enabling RLS and crafting explicit policies you remove authorization logic from application code and gain a defense-in-depth posture. Tooling like Galaxy streamlines writing and auditing policies, but enforcement always lives in the database—exactly where it belongs.
Without row-level security, multi-tenant applications risk leaking sensitive data between users or organizations. Supabase’s RLS lets you enforce access rules at the database layer, eliminating entire classes of security bugs, simplifying application code, and ensuring every access path—from REST to SQL editors—respects the same permissions.
Only for tables that require row isolation. System or lookup tables can keep RLS disabled, but once you enable it you must add explicit policies or all access is denied.
Create a dedicated PostgreSQL role with the BYPASSRLS
attribute or run your ETL inside a function executed with SECURITY DEFINER privileges.
Yes. Galaxy opens a regular Postgres session, so all queries inherit the same RLS policies. You can also test as different roles within Galaxy to validate your rules.
Query SELECT * FROM pg_row_security;
after running your statement, or enable log_statement = 'all'
and look for the rewritten predicate.