add-rls-policies
Generate Row Level Security policies for a Supabase table following Arch Systems department isolation patterns
Files1
View on GitHubSKILL.md.claude/skills/add-rls-policies/SKILL.md
GitHubAdd RLS Policies Skill
Purpose
Generate Row Level Security (RLS) policies for Supabase tables following the Arch Systems department isolation and role-based access patterns.
Process
1. Ask for policy details
- table — Table name (e.g.
operational_delays,breakdowns) - schema — Schema (default:
public) - scope — Access scope:
department— Users can only see/modify rows in their department (most common)admin-only— Only admins can accessowner— Users can only see/modify their own records (e.g. personal tasks)public-read— All authenticated users can read, only department members can write
- operations — Which operations to create policies for: SELECT, INSERT, UPDATE, DELETE
- append-only — Is this table append-only? (like
daily_logs— no UPDATE/DELETE needed) - soft-delete — Should DELETE policy check for
deleted_at IS NULL?
2. Generate the SQL
Department-scoped policies (most common)
-- Enable RLS
ALTER TABLE public.<table> ENABLE ROW LEVEL SECURITY;
-- SELECT: department members + admin
CREATE POLICY "<table>_select_department" ON public.<table>
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM employees e
WHERE e.auth_id = auth.uid()
AND (
e.role = 'admin'
OR e.department_id = <table>.department_id
OR <table>.department_id = ANY(e.accessible_departments)
)
)
);
-- INSERT: department members + admin
CREATE POLICY "<table>_insert_department" ON public.<table>
FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1 FROM employees e
WHERE e.auth_id = auth.uid()
AND (
e.role = 'admin'
OR e.department_id = <table>.department_id
OR <table>.department_id = ANY(e.accessible_departments)
)
)
);
-- UPDATE: department members + admin (optionally restricted to record creator)
CREATE POLICY "<table>_update_department" ON public.<table>
FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM employees e
WHERE e.auth_id = auth.uid()
AND (
e.role = 'admin'
OR e.department_id = <table>.department_id
OR <table>.department_id = ANY(e.accessible_departments)
)
)
);
-- DELETE: admin only (or restrict to supervisors/admins)
CREATE POLICY "<table>_delete_admin" ON public.<table>
FOR DELETE TO authenticated
USING (
EXISTS (
SELECT 1 FROM employees e
WHERE e.auth_id = auth.uid()
AND e.role = 'admin'
)
);
Append-only policies (for logs, audit trails)
-- Same SELECT and INSERT as above
-- NO UPDATE policy — records are immutable
-- NO DELETE policy — records cannot be removed
-- Optionally add a soft-update policy for corrections only:
CREATE POLICY "<table>_update_corrections" ON public.<table>
FOR UPDATE TO authenticated
USING (
EXISTS (
SELECT 1 FROM employees e
WHERE e.auth_id = auth.uid()
AND (e.role = 'admin' OR e.role = 'supervisor')
)
);
Admin-only policies
CREATE POLICY "<table>_select_admin" ON public.<table>
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM employees e
WHERE e.auth_id = auth.uid()
AND e.role = 'admin'
)
);
-- Similar for INSERT, UPDATE, DELETE with admin-only check
Public-read, department-write policies
-- SELECT: all authenticated users
CREATE POLICY "<table>_select_authenticated" ON public.<table>
FOR SELECT TO authenticated
USING (true);
-- INSERT/UPDATE: department members only (same as department-scoped WITH CHECK)
3. Add supporting indexes
Always include these indexes for department-scoped tables:
CREATE INDEX idx_<table>_department ON public.<table>(department_id);
CREATE INDEX idx_<table>_created_at ON public.<table>(created_at DESC);
-- If table has a status column:
CREATE INDEX idx_<table>_status ON public.<table>(status)
WHERE deleted_at IS NULL;
4. Add updated_at trigger (if table has updated_at column)
CREATE TRIGGER update_<table>_updated_at
BEFORE UPDATE ON public.<table>
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
Note: The update_updated_at_column() function should already exist from the initial migration. Only create the trigger.
5. Add to existing migration or create new one
- If adding policies to an existing table, create a new migration file
- If creating a new table with policies, include everything in one migration
- Naming:
packages/database/migrations/NNN_<description>.sql
6. Post-creation
- Push migration:
cd packages/database && pnpm supabase:push - Verify in Supabase Studio that policies appear under Authentication > Policies
- Test with different roles (admin, supervisor, operator)
- Commit the migration file
Policy Naming Convention
<table>_<operation>_<scope>— e.g.,operational_delays_select_department- Always include the table name prefix
- Always include the scope suffix (
_department,_admin,_authenticated,_owner)
Security Checklist
Before finalizing, verify:
- RLS is enabled (
ALTER TABLE ... ENABLE ROW LEVEL SECURITY) - Every table has at minimum a SELECT policy
- INSERT policies include
WITH CHECK(not justUSING) - DELETE is restricted (admin-only or not present for audit tables)
-
department_idcolumn exists on the table for department-scoped policies -
employees.accessible_departmentsis checked for cross-department access - Service role key is NEVER used in client-side code or
NEXT_PUBLIC_*variables