{"manifest":{"name":"Supabase RLS Policy Builder","version":"1.0.0","description":"Write complete Supabase Row Level Security policies for a table from its schema. Generates ENABLE ROW LEVEL SECURITY plus all CRUD policies using the correct auth.uid() patterns, role scoping, and WITH CHECK clauses.","tags":["supabase","rls","postgres","security","sql","skillslap"],"standard":"agentskills.io","standard_version":"1.0","content_checksum":"ae3309e88e03bca073f7fd817f228c4043c2fe0465027919abaaaf023c856465","bundle_checksum":null,"metadata":{},"files":[]},"files":{"SKILL.md":"# Supabase RLS Policy Builder\n\n> **Purpose:** Given a table schema and description of who can access what, write the complete set of production-ready Supabase RLS policies. The output is paste-ready SQL — no gaps, no USING (true) shortcuts on sensitive data.\n\n---\n\n## Invocation\n\n```\n/rls-build <table-schema> [access-rules]\n```\n\n**Examples:**\n- `/rls-build CREATE TABLE skill_reports (id uuid, skill_id uuid, reporter_id uuid, reason text, status text) -- Users can create reports on any active skill; only admins can read all reports; reporters can see their own`\n- `/rls-build skill_collections table -- users own their collections; collections with is_public=true are readable by anyone`\n\n---\n\n## Access Pattern Library\n\nUse these patterns as building blocks. Pick the right one for each operation:\n\n### Pattern A: User owns row (most common)\n```sql\n-- User sees only their own rows\nCREATE POLICY \"[table]_select_own\" ON public.[table]\n  FOR SELECT TO authenticated\n  USING (auth.uid() = user_id);\n\n-- User can only insert rows they own\nCREATE POLICY \"[table]_insert_own\" ON public.[table]\n  FOR INSERT TO authenticated\n  WITH CHECK (auth.uid() = user_id);\n\n-- User can only update their own rows\nCREATE POLICY \"[table]_update_own\" ON public.[table]\n  FOR UPDATE TO authenticated\n  USING (auth.uid() = user_id)\n  WITH CHECK (auth.uid() = user_id);\n\n-- User can only delete their own rows\nCREATE POLICY \"[table]_delete_own\" ON public.[table]\n  FOR DELETE TO authenticated\n  USING (auth.uid() = user_id);\n```\n\n### Pattern B: Public read, owner write\n```sql\n-- Anyone (including anon) can read active rows\nCREATE POLICY \"[table]_select_public\" ON public.[table]\n  FOR SELECT\n  USING (status = 'active');\n\n-- Only the owner can update/delete\nCREATE POLICY \"[table]_update_own\" ON public.[table]\n  FOR UPDATE TO authenticated\n  USING (auth.uid() = author_id)\n  WITH CHECK (auth.uid() = author_id);\n```\n\n### Pattern C: Read own + read public\n```sql\n-- Users see their own rows OR public rows\nCREATE POLICY \"[table]_select\" ON public.[table]\n  FOR SELECT TO authenticated\n  USING (auth.uid() = user_id OR is_public = true);\n```\n\n### Pattern D: Write to parent-owned resource\nUsed when a child row's ownership is determined via a parent JOIN (e.g., inserting a comment on a skill you own):\n```sql\nCREATE POLICY \"[table]_insert_to_own_skill\" ON public.[table]\n  FOR INSERT TO authenticated\n  WITH CHECK (\n    EXISTS (\n      SELECT 1 FROM public.skills\n      WHERE id = skill_id AND author_id = auth.uid()\n    )\n  );\n```\n\n### Pattern E: Authenticated users can create, see their own\nCommon for reports, notifications, submissions:\n```sql\nCREATE POLICY \"[table]_insert\" ON public.[table]\n  FOR INSERT TO authenticated\n  WITH CHECK (auth.uid() = submitter_id);\n\nCREATE POLICY \"[table]_select_own\" ON public.[table]\n  FOR SELECT TO authenticated\n  USING (auth.uid() = submitter_id);\n-- No UPDATE or DELETE — immutable after submission\n```\n\n### Pattern F: Service role for server-side operations\nUse when the server (not the user) writes to a table:\n```sql\n-- Server can do anything (bypasses RLS via service role key)\nCREATE POLICY \"[table]_service\" ON public.[table]\n  USING (auth.role() = 'service_role')\n  WITH CHECK (auth.role() = 'service_role');\n-- Document why service role is needed above this policy\n```\n\n### Pattern G: Admin-only table\n```sql\n-- Read: only if user is in admins list (example: users.is_admin column)\nCREATE POLICY \"[table]_admin_select\" ON public.[table]\n  FOR SELECT TO authenticated\n  USING (\n    EXISTS (\n      SELECT 1 FROM public.users\n      WHERE id = auth.uid() AND is_admin = true\n    )\n  );\n```\n\n---\n\n## Full Template (User-Owned Table)\n\n```sql\n-- RLS for [table]\nALTER TABLE public.[table] ENABLE ROW LEVEL SECURITY;\n\n-- [Brief description of who accesses what]\n\nCREATE POLICY \"[table]_select_own\" ON public.[table]\n  FOR SELECT TO authenticated\n  USING (auth.uid() = user_id);\n\nCREATE POLICY \"[table]_insert_own\" ON public.[table]\n  FOR INSERT TO authenticated\n  WITH CHECK (auth.uid() = user_id);\n\nCREATE POLICY \"[table]_update_own\" ON public.[table]\n  FOR UPDATE TO authenticated\n  USING (auth.uid() = user_id)\n  WITH CHECK (auth.uid() = user_id);\n\nCREATE POLICY \"[table]_delete_own\" ON public.[table]\n  FOR DELETE TO authenticated\n  USING (auth.uid() = user_id);\n```\n\n---\n\n## Critical Rules\n\n1. **Never use `USING (true)` on INSERT** — INSERT policies need `WITH CHECK`, not `USING`\n2. **UPDATE requires BOTH `USING` and `WITH CHECK`** — `USING` checks old row, `WITH CHECK` checks new row\n3. **`auth.uid()` vs `auth.role()`** — uid() returns the user's UUID, role() returns 'anon'/'authenticated'/'service_role'\n4. **Don't mix ALL with specific operations** — use `FOR SELECT`, `FOR INSERT` etc. individually for clarity\n5. **Test anon access** — if anon role should NOT see data, add explicit denial or ensure no policy grants anon access\n\n---\n\n## Output Format\n\n1. **Complete SQL block** — `ENABLE ROW LEVEL SECURITY` + all policies\n2. **Policy rationale** — one-line comment above each policy explaining WHY\n3. **Gaps identified** — call out any operations not covered (e.g., \"No DELETE policy = users cannot delete their reports\")\n"}}