Skip to content

Part 3. Authorization (RLS & Policy)

Goal: Understand how Supabase uses RLS to protect data, write secure policies for each table, and attach metadata (role, org_id) from JWT to policies.

3.1 🎯 Learning Objectives

After completing this section, developers can:

  • Understand how RLS (Row-Level Security) works in Postgres/Supabase.
  • Know how to enable/disable RLS, write policies for CRUD.
  • Use auth.uid() and JWT claims in policies.
  • Design secure multi-tenant (multiple organizations, multiple users).
  • Manage roles (admin, member, guest) at DB layer.

3.2 🔍 Authorization Overview in Supabase

Auth vs Authorization

ConceptRoleHandled Where
AuthenticationVerify user identity (login, token, session)Supabase Auth
AuthorizationDetermine what they are allowed to do (view, edit, delete)RLS Policy in Database

Authorization Architecture

mermaid
flowchart LR
A[Frontend] -->|JWT| B[Supabase API]
B -->|Check Policy| C[(Postgres Table)]
C -->|Evaluate| D{RLS Policy}
D -->|Allow| E[Return data]
D -->|Deny| F[Error: permission denied]

✅ The decision of "who can access" is right in the DB, not in FE code or API.

3.3 🧱 Enable RLS and Basic Policies

Step 1. Enable RLS for Table

sql
alter table profiles enable row level security;

Step 2. Create Policy for SELECT

sql
create policy "Users can view their own profile"
on profiles
for select
using ((select auth.uid()) = id );

✅ Meaning: user can only view records where the record's id matches auth.uid() from JWT.

Step 3. Policy for INSERT

sql
create policy "Users can insert their own profile"
on profiles
for insert
with check ((select auth.uid()) = id );

Step 4. Policy for UPDATE

sql
create policy "Users can update their own profile"
on profiles
for update
using ((select auth.uid()) = id )
with check ((select auth.uid()) = id );

🔎 using checks when reading records, while with check checks when writing/inserting/updating.

3.4 🧩 Helper Functions in Supabase RLS

FunctionDescription
auth.uid()Returns user_id from JWT.
auth.jwt()Returns entire JWT payload as JSON.
auth.role()Returns role of accessing user (usually authenticated or anon).
auth.email()Returns user email if available.

Example:

sql
create policy "Admins can see all"
on users
for select
using ( auth.jwt()->>'role' = 'admin' );

3.5 🏢 Design Authorization for Multi-tenant

Sample Data Model

sql
create table organizations (
  id uuid primary key default gen_random_uuid(),
  name text not null
);

create table members (
  id uuid primary key default gen_random_uuid(),
  organization_id uuid references organizations(id),
  user_id uuid references auth.users(id),
  role text check (role in ('admin', 'member')),
  created_at timestamptz default now()
);

Policy for organizations Table

sql
alter table organizations enable row level security;

create policy "Org members can view their organization"
on organizations
for select
using (
  id in (
    select organization_id from members where user_id = auth.uid()
  )
);

Policy for members Table

sql
alter table members enable row level security;

create policy "Members can view members in their org"
on members
for select
using (
  organization_id in (
    select organization_id from members where user_id = auth.uid()
  )
);

👉 This ensures users only see their organization's data, never other organizations' data - even if a hacker changes the ID.

3.6 🧩 Using JWT Metadata for Role & Org

When User Logs In, JWT Contains Metadata

json
{
  "sub": "user_123",
  "email": "john@example.com",
  "role": "admin",
  "organization_id": "org_456"
}

Policy Using Metadata

sql
create policy "Admins can view all org data"
on organizations
for select
using (
  auth.jwt()->>'role' = 'admin'
);

✅ Role/org_id data is added using updateUser({ data: { role, organization_id } }) in Supabase Auth.

3.7 🧩 Comprehensive Role-based Policy (RBAC)

RoleActionExample Policy
adminView & edit all data in orgauth.jwt()->>'role' = 'admin'
memberOnly view/edit own recordsauth.uid() = created_by
guestOnly read public dataauth.role() = 'anon'

Example: RBAC Policy for tasks Table

sql
alter table tasks enable row level security;

create policy "Admins can manage all tasks"
on tasks for all
using ( auth.jwt()->>'role' = 'admin' );

create policy "Members can manage own tasks"
on tasks for all
using ( auth.uid() = created_by );

3.8 🧮 Testing Policies

Quick Test Method with SQL

sql
-- Simulate user login
set role authenticated;
set jwt.claims.role = 'member';
set jwt.claims.sub = 'user_123';

-- Test SELECT
select * from tasks;

Check via Supabase Dashboard → SQL Editor

  • Run queries as above with Run as Authenticated User.
  • Try different JWT metadata to verify results.

3.9 🧩 Using Policies Combined with Triggers (Audit Log)

Example logging every time user modifies tasks table (INSERT, UPDATE, DELETE)

sql
create table audit_log (
  id bigserial primary key,
  user_id uuid,
  action text,
  table_name text,
  at timestamptz default now()
);

alter table audit_log enable row level security;

create function log_task_access()
returns trigger as $$
begin
  insert into audit_log (user_id, action, table_name)
  values (auth.uid(), TG_OP, TG_TABLE_NAME);
  return new;
end;
$$ language plpgsql;

create trigger trg_log_task_access
after insert or update or delete on tasks
for each statement execute procedure log_task_access();

⚠️ Note: PostgreSQL does not support SELECT triggers. Only INSERT, UPDATE, and DELETE operations can be logged via triggers. To log SELECT operations, consider using RLS policies with logging or application-level logging.

3.10 🧭 Completion Checklist

  • Know how to enable/disable RLS and write basic policies.
  • Understand auth.uid() and auth.jwt().
  • Can write multi-tenant policies.
  • Apply RBAC (admin/member/guest) at DB layer.
  • Test policies using SQL or Supabase Dashboard.
  • Understand how to attach metadata to JWT for flexible policies.

3.11 💡 Internal Best Practices

  1. Always enable RLS for all tables (except system tables).
  2. Don't use service key to bypass RLS except special cases (Edge Function admin).
  3. Each table → have at least 1 clear SELECT, INSERT, UPDATE, DELETE policy.
  4. Don't rely on frontend code to check access permissions.
  5. Metadata in JWT only for context - doesn't replace complex logic checks.
  6. Keep policy files versioned with migrations (migrations/policies.sql).
  7. Test policies every time you add a new table or role.
  8. Avoid writing duplicate policy logic - split by action.
  9. Always review "superuser" (admin) policies to avoid leaking all data.
  10. Use comments in SQL to describe each policy's meaning (useful when onboarding new devs).

3.12 📚 References

3.13 🧾 Output After This Section

After completing Part 3, new developers should:

  • Understand how RLS and policies work.
  • Know how to write policies for each CRUD operation.
  • Can create multi-tenant policies (by organization).
  • Apply RBAC (role-based) using JWT claims.
  • Test and verify each user's access permissions.

Released under the MIT License.