RLS Policy Patterns
Policy Pattern Overview
Common Access Patterns
┌─────────────────────────────────────────────────────────────┐
│ RLS POLICY PATTERNS │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. Owner Access - User owns the row │
│ 2. Role-based Access - Based on user role │
│ 3. Organization Access - Same org can access │
│ 4. Team Access - Team members can access │
│ 5. Public Read - Anyone can read │
│ 6. Hierarchical Access - Based on relationships │
│ 7. Time-based Access - Based on timestamps │
│ │
└─────────────────────────────────────────────────────────────┘
Pattern 1: Owner Access
User Owns Data
-- Table structure
CREATE TABLE public.tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
user_id UUID NOT NULL REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;
-- Policy: Users can CRUD their own tasks
CREATE POLICY "Users manage own tasks"
ON public.tasks
FOR ALL
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
Separate Policies per Operation
-- More granular control
CREATE POLICY "Users can view own tasks"
ON tasks FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create tasks"
ON tasks FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own tasks"
ON tasks FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own tasks"
ON tasks FOR DELETE
USING (auth.uid() = user_id);
Pattern 2: Role-based Access
Admin vs Regular Users
-- Helper function to check role
CREATE OR REPLACE FUNCTION is_admin()
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM public.profiles
WHERE id = auth.uid()
AND role = 'admin'
);
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- Regular users see own data
CREATE POLICY "Users see own data"
ON tasks FOR SELECT
USING (auth.uid() = user_id);
-- Admins see all data
CREATE POLICY "Admins see all data"
ON tasks FOR SELECT
USING (is_admin());
-- Only admins can delete
CREATE POLICY "Only admins can delete"
ON tasks FOR DELETE
USING (is_admin());
Role from JWT
-- Check role from JWT metadata
CREATE POLICY "Premium users access premium content"
ON premium_content FOR SELECT
USING (
(auth.jwt() ->> 'user_metadata')::jsonb ->> 'plan' = 'premium'
);
Pattern 3: Organization Access
Multi-tenant with Organizations
-- Organization members table
CREATE TABLE public.organization_members (
organization_id UUID REFERENCES organizations(id),
user_id UUID REFERENCES auth.users(id),
role TEXT DEFAULT 'member',
PRIMARY KEY (organization_id, user_id)
);
-- Projects belong to organizations
CREATE TABLE public.projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id),
name TEXT NOT NULL
);
-- Helper function
CREATE OR REPLACE FUNCTION user_organizations()
RETURNS SETOF UUID AS $$
SELECT organization_id FROM organization_members
WHERE user_id = auth.uid();
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- Policy: Access projects in your organizations
CREATE POLICY "Org members access projects"
ON projects FOR ALL
USING (organization_id IN (SELECT user_organizations()))
WITH CHECK (organization_id IN (SELECT user_organizations()));
Pattern 4: Team/Group Access
Project Team Members
-- Project members junction table
CREATE TABLE project_members (
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
PRIMARY KEY (project_id, user_id)
);
-- Helper function
CREATE OR REPLACE FUNCTION is_project_member(p_project_id UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM project_members
WHERE project_id = p_project_id
AND user_id = auth.uid()
);
$$ LANGUAGE sql SECURITY DEFINER STABLE;
-- Tasks policies
CREATE POLICY "Project members can view tasks"
ON tasks FOR SELECT
USING (is_project_member(project_id));
CREATE POLICY "Project members can create tasks"
ON tasks FOR INSERT
WITH CHECK (is_project_member(project_id));
Role-specific within Team
-- Only admin/owner can delete
CREATE OR REPLACE FUNCTION is_project_admin(p_project_id UUID)
RETURNS BOOLEAN AS $$
SELECT EXISTS (
SELECT 1 FROM project_members
WHERE project_id = p_project_id
AND user_id = auth.uid()
AND role IN ('owner', 'admin')
);
$$ LANGUAGE sql SECURITY DEFINER STABLE;
CREATE POLICY "Only admins can delete tasks"
ON tasks FOR DELETE
USING (is_project_admin(project_id));
Pattern 5: Public Read, Auth Write
Blog Posts / Public Content
-- Posts table
CREATE TABLE public.posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
author_id UUID REFERENCES auth.users(id),
title TEXT NOT NULL,
content TEXT,
status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'published', 'archived')),
published_at TIMESTAMPTZ
);
-- Anyone can read published posts
CREATE POLICY "Public can read published posts"
ON posts FOR SELECT
USING (status = 'published');
-- Authors can see their own drafts
CREATE POLICY "Authors see own drafts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- Only authors can modify
CREATE POLICY "Authors modify own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Only authenticated users can create
CREATE POLICY "Auth users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
Pattern 6: Hierarchical Access
-- Comments reference posts
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
post_id UUID REFERENCES posts(id) ON DELETE CASCADE,
author_id UUID REFERENCES auth.users(id),
content TEXT NOT NULL
);
-- Anyone can read comments on published posts
CREATE POLICY "Read comments on published posts"
ON comments FOR SELECT
USING (
EXISTS (
SELECT 1 FROM posts
WHERE posts.id = comments.post_id
AND posts.status = 'published'
)
);
-- Comment authors can edit their comments
CREATE POLICY "Authors edit own comments"
ON comments FOR UPDATE
USING (auth.uid() = author_id);
-- Post authors can delete any comment on their posts
CREATE POLICY "Post authors moderate comments"
ON comments FOR DELETE
USING (
EXISTS (
SELECT 1 FROM posts
WHERE posts.id = comments.post_id
AND posts.author_id = auth.uid()
)
);
Pattern 7: Time-based Access
Limited Time Access
-- Invitations with expiry
CREATE TABLE invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES projects(id),
email TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
accepted_at TIMESTAMPTZ
);
-- Only view valid (non-expired) invitations
CREATE POLICY "View valid invitations"
ON invitations FOR SELECT
USING (
expires_at > NOW()
AND accepted_at IS NULL
);
-- Scheduled content
CREATE POLICY "View published content"
ON content FOR SELECT
USING (
published_at IS NOT NULL
AND published_at <= NOW()
AND (expires_at IS NULL OR expires_at > NOW())
);
Pattern 8: Shared Resources
Document Sharing
-- Documents
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id UUID REFERENCES auth.users(id),
title TEXT NOT NULL,
content TEXT
);
-- Document shares
CREATE TABLE document_shares (
document_id UUID REFERENCES documents(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
permission TEXT CHECK (permission IN ('view', 'edit')),
PRIMARY KEY (document_id, user_id)
);
-- Owner access
CREATE POLICY "Owners have full access"
ON documents FOR ALL
USING (auth.uid() = owner_id);
-- Shared view access
CREATE POLICY "Shared users can view"
ON documents FOR SELECT
USING (
id IN (
SELECT document_id FROM document_shares
WHERE user_id = auth.uid()
)
);
-- Shared edit access
CREATE POLICY "Shared users with edit can update"
ON documents FOR UPDATE
USING (
id IN (
SELECT document_id FROM document_shares
WHERE user_id = auth.uid()
AND permission = 'edit'
)
);
Optimize Policy Queries
-- ❌ SLOW: Subquery in every row
CREATE POLICY "slow_policy"
ON tasks FOR SELECT
USING (
project_id IN (
SELECT project_id FROM project_members
WHERE user_id = auth.uid()
)
);
-- ✅ FASTER: Use function (can be cached)
CREATE OR REPLACE FUNCTION user_project_ids()
RETURNS SETOF UUID AS $$
SELECT project_id FROM project_members
WHERE user_id = auth.uid();
$$ LANGUAGE sql SECURITY DEFINER STABLE;
CREATE POLICY "fast_policy"
ON tasks FOR SELECT
USING (project_id IN (SELECT user_project_ids()));
Index for RLS
-- Index columns used in policies
CREATE INDEX idx_tasks_user_id ON tasks(user_id);
CREATE INDEX idx_tasks_project_id ON tasks(project_id);
CREATE INDEX idx_project_members_user ON project_members(user_id);
Tổng kết
Pattern Selection Guide
| Pattern |
Use When |
| Owner Access |
Personal data (tasks, notes) |
| Role-based |
Admin/user distinction |
| Organization |
Multi-tenant apps |
| Team Access |
Collaborative projects |
| Public Read |
Blog, public content |
| Hierarchical |
Nested resources |
| Time-based |
Invites, scheduled content |
| Shared |
Document sharing |
Best Practices
✅ Use SECURITY DEFINER functions for complex checks
✅ Index columns used in policies
✅ Keep policies simple and readable
✅ Test with different user roles
✅ Multiple policies = OR (any passes = access)
Q&A
- Pattern nào phù hợp với dự án?
- Có cần combine nhiều patterns?
- Performance concerns với complex policies?