惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
K
Kaspersky official blog
Forbes - Security
Forbes - Security
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
H
Heimdal Security Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
V
Vulnerabilities – Threatpost
T
Tenable Blog
T
Tailwind CSS Blog
P
Privacy International News Feed
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
Vercel News
Vercel News
A
About on SuperTechFans
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 司徒正美
量子位
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
Project Zero
Project Zero

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Supabase RLS Policy Design Patterns Beyond the Basics
Mahdi BEN RHOUMA · 2026-06-18 · via DEV Community

Supabase RLS Policy Design Patterns Beyond the Basics

Row Level Security is one of the most powerful features in Supabase — and one of the most misunderstood. Most tutorials stop at auth.uid() = user_id. That gets you through a simple personal data model, but the moment you introduce teams, roles, organizations, or any shared resource, you need a more structured approach.

This guide covers advanced RLS policy patterns used in real production apps: multi-role systems, team-based access control, hierarchical permissions, and how to keep policies performant as your data grows. Every pattern includes copy-paste SQL you can adapt immediately.

Estimated read time: 16 minutes

Prerequisites

  • Supabase project with Auth enabled
  • Familiarity with basic RLS (CREATE POLICY, auth.uid())
  • Basic PostgreSQL knowledge (JOINs, functions)
  • Next.js app using @supabase/ssr or @supabase/auth-helpers-nextjs

How RLS Actually Works (The Mental Model You Need)

Before patterns, get this straight: RLS policies are predicate filters appended to every query. When you write:

CREATE POLICY "users can read own data"
ON profiles
FOR SELECT
USING (auth.uid() = user_id);

Postgres rewrites every SELECT on profiles to include WHERE auth.uid() = user_id. This happens at the database level — no application code can bypass it (unless using the service role key).

Two clauses matter:

  • USING — filters rows for SELECT, UPDATE, DELETE
  • WITH CHECK — validates rows on INSERT, UPDATE

Always define both for tables that accept writes.


Pattern 1: Role-Based Access with a Roles Table

The naive approach is storing roles in user_metadata. Don't. Metadata is controlled by the client and can be spoofed. Store roles in a database table.

-- roles table
CREATE TABLE user_roles (
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('admin', 'editor', 'viewer')),
  PRIMARY KEY (user_id)
);

-- helper function (security definer = runs as postgres, not the calling user)
CREATE OR REPLACE FUNCTION get_user_role()
RETURNS TEXT
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
  SELECT role FROM user_roles WHERE user_id = auth.uid();
$$;

Now use it in policies:

-- admins can read everything, others read only published content
CREATE POLICY "role-based content access"
ON articles
FOR SELECT
USING (
  get_user_role() = 'admin'
  OR status = 'published'
);

-- only admins and editors can insert
CREATE POLICY "editors can create articles"
ON articles
FOR INSERT
WITH CHECK (
  get_user_role() IN ('admin', 'editor')
);

The SECURITY DEFINER function is critical here. Without it, the subquery inside the policy runs as the calling user, which can cause permission errors on the user_roles table itself.

[INTERNAL LINK: supabase-authentication-authorization]


Pattern 2: Team / Organization Membership

This is the most common pattern in SaaS apps. Users belong to organizations, and resources belong to organizations.

-- org membership table
CREATE TABLE org_members (
  org_id UUID NOT NULL,
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
  PRIMARY KEY (org_id, user_id)
);

-- projects belong to orgs
CREATE TABLE projects (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL,
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

Policy: members of an org can read its projects.

CREATE POLICY "org members can read projects"
ON projects
FOR SELECT
USING (
  EXISTS (
    SELECT 1 FROM org_members
    WHERE org_members.org_id = projects.org_id
    AND org_members.user_id = auth.uid()
  )
);

-- only org admins/owners can create projects
CREATE POLICY "org admins can create projects"
ON projects
FOR INSERT
WITH CHECK (
  EXISTS (
    SELECT 1 FROM org_members
    WHERE org_members.org_id = projects.org_id
    AND org_members.user_id = auth.uid()
    AND org_members.role IN ('owner', 'admin')
  )
);

Performance note: Add an index on org_members(user_id) and org_members(org_id). The EXISTS subquery runs on every row evaluation — without indexes, this becomes a full table scan at scale.

CREATE INDEX idx_org_members_user_id ON org_members(user_id);
CREATE INDEX idx_org_members_org_id ON org_members(org_id);


Pattern 3: Hierarchical Permissions (Owner > Admin > Member)

Sometimes you need cascading permissions: owners can do everything admins can, admins can do everything members can. A helper function keeps policies clean.

CREATE OR REPLACE FUNCTION user_org_role(p_org_id UUID)
RETURNS TEXT
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
  SELECT role FROM org_members
  WHERE org_id = p_org_id AND user_id = auth.uid();
$$;

-- helper: check if user has at least a given role level
CREATE OR REPLACE FUNCTION user_has_org_role(p_org_id UUID, p_min_role TEXT)
RETURNS BOOLEAN
LANGUAGE sql
STABLE
SECURITY DEFINER
AS $$
  SELECT CASE
    WHEN p_min_role = 'member' THEN
      user_org_role(p_org_id) IN ('member', 'admin', 'owner')
    WHEN p_min_role = 'admin' THEN
      user_org_role(p_org_id) IN ('admin', 'owner')
    WHEN p_min_role = 'owner' THEN
      user_org_role(p_org_id) = 'owner'
    ELSE false
  END;
$$;

Now policies read like documentation:

-- delete requires owner
CREATE POLICY "owners can delete projects"
ON projects
FOR DELETE
USING (user_has_org_role(org_id, 'owner'));

-- update requires admin or above
CREATE POLICY "admins can update projects"
ON projects
FOR UPDATE
USING (user_has_org_role(org_id, 'admin'))
WITH CHECK (user_has_org_role(org_id, 'admin'));


Pattern 4: Resource Sharing and Invitations

Users sometimes need access to specific resources without being full org members — think shared documents or guest access.

CREATE TABLE resource_shares (
  resource_id UUID NOT NULL,
  resource_type TEXT NOT NULL,
  shared_with UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  permission TEXT NOT NULL CHECK (permission IN ('read', 'write')),
  expires_at TIMESTAMPTZ,
  PRIMARY KEY (resource_id, shared_with)
);

Policy that combines ownership and sharing:

CREATE POLICY "owners and shared users can read documents"
ON documents
FOR SELECT
USING (
  -- owner
  owner_id = auth.uid()
  OR
  -- explicitly shared
  EXISTS (
    SELECT 1 FROM resource_shares
    WHERE resource_shares.resource_id = documents.id
    AND resource_shares.resource_type = 'document'
    AND resource_shares.shared_with = auth.uid()
    AND (resource_shares.expires_at IS NULL OR resource_shares.expires_at > now())
  )
);


Pattern 5: Public + Authenticated Mixed Access

A common pattern for content sites: public content is readable by anyone, private content only by the owner.

CREATE POLICY "public content is readable by all"
ON posts
FOR SELECT
USING (
  visibility = 'public'
  OR author_id = auth.uid()
);

For anonymous users, auth.uid() returns NULL. The OR author_id = auth.uid() clause evaluates to NULL = NULL which is false in SQL — so anonymous users only see public posts. This is correct behavior, but it's worth being explicit about.

If you want to allow anonymous reads of public content but block all writes:

-- no INSERT policy = nobody can insert (including authenticated users)
-- add explicit policies only for the roles that need write access

Absence of a policy is a deny. This is the default-deny model that makes RLS safe.


Pattern 6: Audit Trails Without Bypassing RLS

A common mistake: using the service role in a trigger to write audit logs, which bypasses RLS on the audit table. Instead, use a SECURITY DEFINER function that writes to the audit table directly.

CREATE TABLE audit_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  table_name TEXT,
  operation TEXT,
  row_id UUID,
  changed_by UUID,
  changed_at TIMESTAMPTZ DEFAULT now(),
  old_data JSONB,
  new_data JSONB
);

-- RLS: only admins can read audit logs
ALTER TABLE audit_log ENABLE ROW LEVEL SECURITY;

CREATE POLICY "admins read audit log"
ON audit_log
FOR SELECT
USING (get_user_role() = 'admin');

-- trigger function runs as postgres (SECURITY DEFINER)
CREATE OR REPLACE FUNCTION log_changes()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
  INSERT INTO audit_log (table_name, operation, row_id, changed_by, old_data, new_data)
  VALUES (
    TG_TABLE_NAME,
    TG_OP,
    COALESCE(NEW.id, OLD.id),
    auth.uid(),
    CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD)::jsonb ELSE NULL END,
    CASE WHEN TG_OP != 'DELETE' THEN row_to_json(NEW)::jsonb ELSE NULL END
  );
  RETURN COALESCE(NEW, OLD);
END;
$$;

[INTERNAL LINK: supabase-postgres-functions-triggers-guide]


Common Pitfalls

Forgetting WITH CHECK on UPDATE policies. USING controls which rows can be targeted, but WITH CHECK controls what the row looks like after the update. Without WITH CHECK, a user could update a row they own to assign it to another user.

Using auth.jwt() claims for authorization. JWT claims are set at login time and don't reflect real-time role changes. A user whose role was revoked still has the old claim until their token expires. Use database lookups via SECURITY DEFINER functions instead.

Not testing with the anon key. Always test your policies using the anon key (not the service role) in the Supabase dashboard SQL editor. The service role bypasses everything.

Policies on tables without indexes. Every policy condition is evaluated per row. A subquery in a policy without an index causes a full table scan for every row in the outer query. Profile with EXPLAIN ANALYZE before going to production.

Multiple permissive policies are OR'd together. If you have two FOR SELECT policies on the same table, a row is visible if either policy passes. This is often surprising. Use a single policy with explicit OR conditions if you want clear logic.


Testing Your Policies

Use the Supabase SQL editor with SET LOCAL role = authenticated and SET LOCAL request.jwt.claims = '{"sub": "<user-id>"}' to simulate a specific user:

-- simulate a specific user
SET LOCAL role = authenticated;
SET LOCAL "request.jwt.claims" = '{"sub": "your-user-uuid-here"}';

-- now run your query — it will respect RLS as that user
SELECT * FROM projects WHERE org_id = 'your-org-id';

This is far faster than testing through your application layer.


Summary and Next Steps

Advanced RLS comes down to a few principles: store roles in the database (not JWT claims), use SECURITY DEFINER functions to encapsulate expensive lookups, index every column used in policy conditions, and always test with the anon key.

The patterns here — role tables, org membership, hierarchical roles, resource sharing — cover the vast majority of real-world authorization requirements. Combine them as needed for your data model.

Related reading:

  • [INTERNAL LINK: nextjs-supabase-advanced-authentication-patterns]
  • [INTERNAL LINK: nextjs-supabase-security-best-practices]
  • [INTERNAL LINK: nextjs-supabase-multi-tenant-saas-architecture]

Originally published at https://www.iloveblogs.blog