DEV Community

kanta13jp1
kanta13jp1

Posted on

Supabase RLS Advanced Patterns — Team Sharing, Multi-Tenancy, and Admin Access

Supabase RLS Advanced Patterns — Team Sharing, Multi-Tenancy, and Admin Access

Go beyond "only see your own data" and model complex permission structures with RLS.

Team Sharing Pattern

-- Team membership table
CREATE TABLE team_members (
  team_id  UUID NOT NULL,
  user_id  UUID NOT NULL REFERENCES auth.users,
  role     TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
  PRIMARY KEY (team_id, user_id)
);

-- All team members can read projects
CREATE POLICY "team members can view projects"
  ON projects FOR SELECT
  USING (
    team_id IN (
      SELECT team_id FROM team_members
      WHERE user_id = auth.uid()
    )
  );

-- Only owners and editors can update
CREATE POLICY "editors can update projects"
  ON projects FOR UPDATE
  USING (
    team_id IN (
      SELECT team_id FROM team_members
      WHERE user_id = auth.uid()
        AND role IN ('owner', 'editor')
    )
  );
Enter fullscreen mode Exit fullscreen mode

Multi-Tenancy: Isolate by org_id

-- Read org_id from a JWT custom claim
CREATE OR REPLACE FUNCTION get_org_id()
RETURNS UUID AS $$
  SELECT (auth.jwt() ->> 'org_id')::UUID;
$$ LANGUAGE SQL STABLE;

-- Restrict all access to the current org
CREATE POLICY "org isolation"
  ON tasks FOR ALL
  USING (org_id = get_org_id());
Enter fullscreen mode Exit fullscreen mode
// Attach custom claims in an Edge Function or via app_metadata
const { data: { session } } = await supabase.auth.getSession();
// supabase.auth.updateUser() with app_metadata: { org_id: '...' }
Enter fullscreen mode Exit fullscreen mode

Admin Access Without service_role

-- Admin whitelist table
CREATE TABLE admins (
  user_id UUID PRIMARY KEY REFERENCES auth.users
);

-- Admins can see everything; owners can still see their own data
CREATE POLICY "admins can view all"
  ON projects FOR SELECT
  USING (
    auth.uid() IN (SELECT user_id FROM admins)
    OR owner_id = auth.uid()
  );
Enter fullscreen mode Exit fullscreen mode

RLS Performance Optimization

-- SLOW: subquery is evaluated for every row
USING (team_id IN (SELECT team_id FROM team_members WHERE user_id = auth.uid()));

-- FAST: rewrite as EXISTS so the planner can use indexes
USING (
  EXISTS (
    SELECT 1 FROM team_members
    WHERE team_id = projects.team_id
      AND user_id = auth.uid()
  )
);

-- Always add a supporting index
CREATE INDEX idx_team_members_user_id ON team_members(user_id);
Enter fullscreen mode Exit fullscreen mode

Summary

Team sharing    → team_members table + role column for RBAC
Multi-tenancy   → JWT custom claim (org_id) for org isolation
Admin access    → admins table + OR condition layered on top
Performance     → EXISTS + index beats IN subquery every time
Enter fullscreen mode Exit fullscreen mode

RLS is the single choke point for "who can see what." Always validate your policies with tests.

Top comments (0)