# Supabase RLS Auditor

> **Purpose:** Audit a Supabase database migration file or SQL containing Row Level Security (RLS) policies for correctness and security. Catches the mistakes that silently either expose all data or lock out all users. Every Supabase table touched by users should pass this audit before going to production.

---

## Invocation

```
/rls-audit <migration-file-or-sql>
```

---

## The 8-Point RLS Checklist

### Check 1: RLS Enabled on Every Table

Every table that stores user data must have:
```sql
ALTER TABLE [table_name] ENABLE ROW LEVEL SECURITY;
```

**Failure:** Table has no RLS — all authenticated users can read/write all rows.

---

### Check 2: No Unrestricted `USING (true)` on Sensitive Tables

```sql
-- ❌ BAD — exposes ALL rows to ALL authenticated users
CREATE POLICY "all_read" ON users FOR SELECT USING (true);

-- ✅ GOOD — only user's own rows
CREATE POLICY "own_rows" ON users FOR SELECT USING (auth.uid() = user_id);
```

**Exception:** Public tables (e.g., `skills` with `status = 'active'`) may use `USING (true)` for SELECT only.

---

### Check 3: INSERT Policies Have `WITH CHECK`

```sql
-- ❌ BAD — INSERT policy with no WITH CHECK
CREATE POLICY "insert_tip" ON tips FOR INSERT TO authenticated USING (true);

-- ✅ GOOD
CREATE POLICY "insert_tip" ON tips FOR INSERT TO authenticated
  WITH CHECK (auth.uid() = tipper_id);
```

---

### Check 4: No Anon Role Access to Private Tables

Policies with `TO anon` on tables containing user data are dangerous. Flag any policy where:
- Role is `anon` or omitted (defaults to all roles including anon)
- Table contains PII (email, phone, address) or financial data

---

### Check 5: Service Role Bypasses Are Documented

```sql
-- This bypasses ALL RLS — it should be intentional and documented
CREATE POLICY "service_role_all" ON [table]
  USING (auth.role() = 'service_role');
```

Flag: every service-role bypass needs a comment explaining WHY it's needed.

---

### Check 6: auth.uid() Typo Check

Common typo that silently evaluates to `NULL = user_id` (always false — locks out all users):
```sql
-- ❌ BAD TYPOS
USING (auth.uid = user_id)         -- missing () → NULL
USING (auth.uid() = userId)        -- camelCase column name
USING (auth_uid() = user_id)       -- underscore instead of dot

-- ✅ CORRECT
USING (auth.uid() = user_id)
```

---

### Check 7: Separate Policies per Operation

```sql
-- ❌ BAD — FOR ALL lets users UPDATE and DELETE other users' data if USING passes
CREATE POLICY "user_access" ON orders FOR ALL USING (auth.uid() = user_id);

-- ✅ GOOD — explicit per-operation policies
CREATE POLICY "select_own" ON orders FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "insert_own" ON orders FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "update_own" ON orders FOR UPDATE USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);
```

---

### Check 8: Recursive Policy Risk

Policies that query the same table they protect can cause infinite recursion. Flag any policy that contains a subquery on the same table.

---

## Output Format

```
## RLS Audit: [migration file name]

### Tables Audited: N

---

#### [table_name]

| Check | Status | Notes |
|-------|--------|-------|
| RLS enabled | ✅ / ❌ FAIL | |
| No unrestricted USING | ✅ / ⚠️ REVIEW | |
| INSERT has WITH CHECK | ✅ / ❌ FAIL | |
| No anon leaks | ✅ / ❌ FAIL | |
| Service bypasses documented | ✅ / ❌ FAIL | |
| auth.uid() correct | ✅ / ❌ FAIL | |
| Per-operation policies | ✅ / ⚠️ REVIEW | |
| No recursion risk | ✅ / ❌ FAIL | |

**[table_name] verdict:** PASS / FAIL / NEEDS REVIEW

---

### Overall Audit Result: PASS / FAIL

Critical issues (must fix before production): [N]
Warnings (review before production): [N]

### Fix SQL

[SQL to fix any FAIL items]
```

---

## Rules

- Check 1 (RLS enabled) failure is always CRITICAL — no exceptions
- Check 2 (USING true) on private tables is always CRITICAL
- Flag any policy targeting `FOR ALL` — it's almost always wrong
- If a table has no policies at all (but RLS is enabled): that's a lockout — all inserts/selects will fail silently

## Playground

<!DOCTYPE html><html><head><meta charset='utf-8'><style>*{box-sizing:border-box;margin:0;padding:0}body{background:#0d1117;color:#e6edf3;font-family:monospace;font-size:12px;height:100vh;display:flex;flex-direction:column;overflow:hidden}.header{background:#161b22;border-bottom:1px solid #30363d;padding:8px 14px;font-size:11px;color:#8b949e;display:flex;justify-content:space-between;align-items:center;flex-shrink:0}.title{color:#58a6ff;font-weight:bold;font-size:13px}.panels{display:flex;flex:1;overflow:hidden}.panel{flex:1;overflow:auto;padding:12px;border-right:1px solid #30363d}.panel:last-child{border-right:none}.label{font-size:10px;color:#8b949e;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px}pre{white-space:pre-wrap;word-break:break-word;line-height:1.5}</style></head><body><div class='header'><span class='title'>Supabase RLS Auditor</span><span>Example · SkillSlap</span></div><div class='panels'><div class='panel'><div class='label'>Input: RLS policy</div><pre><span style='color:#8b949e'>-- skills table</span>
<span style='color:#8b949e'>CREATE POLICY "read_public"</span>
<span style='color:#8b949e'>  ON skills FOR SELECT</span>
<span style='color:#8b949e'>  USING (true);</span>

<span style='color:#8b949e'>CREATE POLICY "author_update"</span>
<span style='color:#8b949e'>  ON skills FOR UPDATE</span>
<span style='color:#8b949e'>  USING (author_id = auth.uid());</span></pre></div><div class='panel'><div class='label'>Output: Audit findings</div><pre><span style='color:#f85149'>🔴 read_public: USING(true)</span>
<span style='color:#f85149'>Returns ALL rows including unlisted</span>
<span style='color:#f85149'>and draft skills to any anon user.</span>
<span style='color:#f85149'>Fix: USING(visibility = 'public')</span>

<span style='color:#3fb950'>✅ author_update: correct</span>
<span style='color:#8b949e'>Only skill author can UPDATE. Good.</span>

<span style='color:#e3b341'>🟡 Missing: DELETE policy</span>
<span style='color:#e3b341'>Explicit policy documents intent:</span>
<span style='color:#e3b341'>CREATE POLICY "author_delete"</span>
<span style='color:#e3b341'>  ON skills FOR DELETE</span>
<span style='color:#e3b341'>  USING (author_id = auth.uid());</span>

<span style='color:#e3b341'>🟡 Missing: INSERT policy for authors</span></pre></div></div></body></html>