Supabase Policy Guardrails Overview Organizational governance for Supabase at scale: a shared RLS policy library (reusable templates for common access patterns), naming conventions (tables, columns, functions, policies), migration review process (CI checks ensuring RLS, preventing destructive operations, enforcing naming), cost alert configuration (billing thresholds and usage monitoring), and security audit scripts (scanning for exposed keys, missing RLS, overly permissive policies). All patterns use real from and Supabase CLI commands. Prerequisites - Supabase project with CLI installed and…

-- not plural (heuristic)\n )\n AND t.tablename NOT LIKE '\\_%'; -- skip internal tables\n\n -- Columns must be snake_case\n RETURN QUERY\n SELECT\n 'Column name should be snake_case'::text,\n (c.table_name || '.' || c.column_name)::text,\n regexp_replace(c.column_name, '([A-Z])', '_\\1', 'g')::text\n FROM information_schema.columns c\n WHERE c.table_schema = 'public'\n AND (c.column_name ~ '[A-Z]' OR c.column_name ~ '-');\n\n -- Foreign key columns should end with _id\n RETURN QUERY\n SELECT\n 'Foreign key column should end with _id'::text,\n (tc.table_name || '.' || kcu.column_name)::text,\n (kcu.column_name || '_id')::text\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name\n WHERE tc.constraint_type = 'FOREIGN KEY'\n AND tc.table_schema = 'public'\n AND kcu.column_name NOT LIKE '%_id';\n\n -- Boolean columns should start with is_ or has_\n RETURN QUERY\n SELECT\n 'Boolean column should start with is_ or has_'::text,\n (c.table_name || '.' || c.column_name)::text,\n ('is_' || c.column_name)::text\n FROM information_schema.columns c\n WHERE c.table_schema = 'public'\n AND c.data_type = 'boolean'\n AND c.column_name NOT LIKE 'is_%'\n AND c.column_name NOT LIKE 'has_%';\nEND;\n$ LANGUAGE plpgsql;\n\n-- Run: SELECT * FROM public.validate_naming_conventions();\n```\n\n### Naming Convention Reference\n\n| Object | Convention | Example |\n|--------|-----------|---------|\n| Tables | Plural snake_case | `user_profiles`, `order_items` |\n| Columns | snake_case | `created_at`, `full_name` |\n| Foreign keys | `{referenced_table_singular}_id` | `user_id`, `order_id` |\n| Booleans | `is_` or `has_` prefix | `is_active`, `has_verified_email` |\n| Timestamps | `_at` suffix | `created_at`, `updated_at`, `deleted_at` |\n| RLS policies | `{scope}_{operation}` | `owner_select`, `org_insert` |\n| Functions | `verb_noun` | `create_user`, `get_dashboard_metrics` |\n| Indexes | `idx_{table}_{columns}` | `idx_orders_user_id_created_at` |\n| Migrations | `{timestamp}_{verb}_{description}` | `20250322000000_create_orders_table.sql` |\n\n## Step 2 — Migration Review Process with CI Checks\n\nSee [CI checks, cost alerts, and security audits](references/ci-cost-security.md) for GitHub Actions migration guardrails (RLS enforcement, naming checks, destructive operation blocks), pre-commit hooks, cost monitoring with Slack alerts, security audit scripts, and scheduled Edge Function audits.\n\n## Output\n\n- Shared RLS policy library with owner-only, org-scoped, and public-read templates\n- Naming convention validation function checking tables, columns, FKs, and booleans\n- CI pipeline enforcing RLS, naming, and destructive operation controls\n- Pre-commit hook blocking hardcoded secrets and tables without RLS\n- Cost monitoring script with configurable thresholds and Slack alerting\n- Security audit script detecting missing RLS, permissive policies, and missing indexes\n- Scheduled Edge Function for continuous security monitoring\n\n## Error Handling\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| CI RLS check fails on new table | Migration missing `ENABLE ROW LEVEL SECURITY` | Add `ALTER TABLE` after `CREATE TABLE` in same migration |\n| Naming convention false positive | Table is intentionally singular (e.g., `config`) | Add to exclusion list in validation function |\n| Cost alert not firing | Missing `SUPABASE_ACCESS_TOKEN` | Generate token at supabase.com/dashboard/account/tokens |\n| Security audit times out | Too many tables to scan | Run audit on specific schemas or paginate results |\n| Pre-commit blocks legitimate JWT in test | Test fixture contains JWT-like string | Add test file path to exclusion pattern |\n| RLS template function not found | Migration not applied | Run `supabase db reset` or apply migration manually |\n\n## Examples\n\nSee [CI, cost, and security reference](references/ci-cost-security.md) for full examples including applying RLS templates, running security audits, and checking naming conventions.\n\n## Resources\n\n- [Supabase Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)\n- [Supabase CLI Migrations](https://supabase.com/docs/guides/cli/managing-environments)\n- [Supabase Management API](https://supabase.com/docs/reference/api/introduction)\n- [Supabase Pricing](https://supabase.com/pricing)\n- [PostgreSQL Naming Conventions](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)\n\n## Next Steps\n\nFor architecture patterns across different app types, see `supabase-architecture-variants`.\n---","attachment_filenames":["references/ci-cost-security.md","references/errors.md","references/eslint-rules.md","references/examples.md"],"attachments":[{"filename":"references/ci-cost-security.md","content":"## Migration Review, Cost Alerts, and Security Audit\n\n### GitHub Actions Migration Guardrails\n\n```yaml\n# .github/workflows/supabase-guardrails.yml\nname: Supabase Migration Guardrails\n\non:\n pull_request:\n paths:\n - 'supabase/migrations/**'\n\njobs:\n migration-review:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: supabase/setup-cli@v1\n\n - name: Start local Supabase\n run: supabase start\n\n - name: Apply migrations\n run: supabase db reset\n\n - name: Check RLS enabled on all public tables\n run: |\n MISSING_RLS=$(supabase db query \"\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND rowsecurity = false\n AND tablename NOT LIKE '\\_%'\n AND tablename NOT IN ('schema_migrations')\n \" --output csv | tail -n +2)\n\n if [ -n \"$MISSING_RLS\" ]; then\n echo \"::error::Tables missing RLS: $MISSING_RLS\"\n echo \"Fix: ALTER TABLE public.\u003ctable> ENABLE ROW LEVEL SECURITY;\"\n exit 1\n fi\n echo \"All public tables have RLS enabled\"\n\n - name: Check migration naming convention\n run: |\n for file in supabase/migrations/*.sql; do\n basename=$(basename \"$file\")\n if ! echo \"$basename\" | grep -qE '^[0-9]{14}_(create|alter|drop|add|remove|update|fix|seed|enable|disable)_[a-z_]+\\.sql

Supabase Policy Guardrails Overview Organizational governance for Supabase at scale: a shared RLS policy library (reusable templates for common access patterns), naming conventions (tables, columns, functions, policies), migration review process (CI checks ensuring RLS, preventing destructive operations, enforcing naming), cost alert configuration (billing thresholds and usage monitoring), and security audit scripts (scanning for exposed keys, missing RLS, overly permissive policies). All patterns use real from and Supabase CLI commands. Prerequisites - Supabase project with CLI installed and…

; then\n echo \"::error::Migration '$basename' violates naming convention\"\n echo \"Expected: \u003c14-digit-timestamp>_\u003cverb>_\u003cdescription>.sql\"\n exit 1\n fi\n done\n echo \"Migration naming convention check passed\"\n\n - name: Block unannotated destructive operations\n run: |\n for file in supabase/migrations/*.sql; do\n if grep -qiE 'DROP TABLE|DROP COLUMN|TRUNCATE|DELETE FROM.*WHERE\\s+(1=1|true)' \"$file\"; then\n if ! grep -qi '-- APPROVED-DESTRUCTIVE:' \"$file\"; then\n echo \"::error::Destructive operation in $file without approval annotation\"\n echo \"Add '-- APPROVED-DESTRUCTIVE: \u003creason>' to acknowledge\"\n exit 1\n fi\n fi\n done\n echo \"Destructive operation check passed\"\n\n - name: Validate naming conventions\n run: |\n ISSUES=$(supabase db query \"SELECT * FROM public.validate_naming_conventions()\" --output csv | tail -n +2)\n if [ -n \"$ISSUES\" ]; then\n echo \"::warning::Naming convention issues found:\"\n echo \"$ISSUES\"\n fi\n\n - name: Check foreign key indexes\n run: |\n MISSING_INDEXES=$(supabase db query \"\n SELECT\n tc.table_name,\n kcu.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name\n LEFT JOIN pg_indexes pi\n ON pi.tablename = tc.table_name\n AND pi.indexdef LIKE '%' || kcu.column_name || '%'\n WHERE tc.constraint_type = 'FOREIGN KEY'\n AND tc.table_schema = 'public'\n AND pi.indexname IS NULL\n \" --output csv | tail -n +2)\n\n if [ -n \"$MISSING_INDEXES\" ]; then\n echo \"::warning::Foreign key columns missing indexes: $MISSING_INDEXES\"\n fi\n\n - name: Stop Supabase\n if: always()\n run: supabase stop\n```\n\n### Pre-Commit Hook for Secrets and SQL Lint\n\n```bash\n#!/bin/bash\n# scripts/supabase-pre-commit.sh\nset -euo pipefail\n\necho \"Running Supabase pre-commit checks...\"\n\nSTAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)\n\n# Check 1: No hardcoded Supabase keys (JWT format)\nif echo \"$STAGED_FILES\" | grep -v '.env' | grep -v 'pnpm-lock' | \\\n xargs grep -lE 'eyJ[A-Za-z0-9_-]{50,}\\.' 2>/dev/null; then\n echo \"ERROR: Possible Supabase API key in staged files\"\n echo \"Use environment variables instead\"\n exit 1\nfi\n\n# Check 2: No connection strings\nif echo \"$STAGED_FILES\" | xargs grep -lE 'postgres://postgres\\.[a-z]+:' 2>/dev/null; then\n echo \"ERROR: Supabase connection string in staged files\"\n exit 1\nfi\n\n# Check 3: Migration files have RLS (new tables)\nfor file in $(echo \"$STAGED_FILES\" | grep 'supabase/migrations/.*\\.sql

Supabase Policy Guardrails Overview Organizational governance for Supabase at scale: a shared RLS policy library (reusable templates for common access patterns), naming conventions (tables, columns, functions, policies), migration review process (CI checks ensuring RLS, preventing destructive operations, enforcing naming), cost alert configuration (billing thresholds and usage monitoring), and security audit scripts (scanning for exposed keys, missing RLS, overly permissive policies). All patterns use real from and Supabase CLI commands. Prerequisites - Supabase project with CLI installed and…

|| true); do\n if grep -qi 'CREATE TABLE public\\.' \"$file\"; then\n if ! grep -qi 'ENABLE ROW LEVEL SECURITY' \"$file\"; then\n echo \"ERROR: $file creates a table without enabling RLS\"\n echo \"Add: ALTER TABLE public.\u003ctable> ENABLE ROW LEVEL SECURITY;\"\n exit 1\n fi\n fi\ndone\n\necho \"Supabase pre-commit checks passed\"\n```\n\n```bash\n# Install with Husky\nnpx husky add .husky/pre-commit 'bash scripts/supabase-pre-commit.sh'\n```\n\n## Step 3 — Cost Alerts and Security Audit Scripts\n\n### Cost Alert Configuration\n\n```typescript\n// scripts/supabase-cost-monitor.ts\nimport { createClient } from '@supabase/supabase-js'\n\n// Use the Supabase Management API for cost monitoring\nconst SUPABASE_ACCESS_TOKEN = process.env.SUPABASE_ACCESS_TOKEN!\nconst PROJECT_REF = process.env.SUPABASE_PROJECT_REF!\n\ninterface UsageMetrics {\n database_size_gb: number\n storage_size_gb: number\n bandwidth_gb: number\n edge_function_invocations: number\n monthly_active_users: number\n}\n\n// Cost thresholds — adjust per your budget\nconst THRESHOLDS = {\n database_size_gb: 8, // Pro includes 8 GB\n storage_size_gb: 100, // Pro includes 100 GB\n bandwidth_gb: 250, // Pro includes 250 GB\n edge_function_invocations: 2_000_000, // Pro includes 2M\n monthly_active_users: 100_000, // Pro limit\n}\n\nasync function checkCostAlerts() {\n // Fetch current usage via Supabase Management API\n const response = await fetch(\n `https://api.supabase.com/v1/projects/${PROJECT_REF}/usage`,\n {\n headers: { Authorization: `Bearer ${SUPABASE_ACCESS_TOKEN}` },\n }\n )\n\n if (!response.ok) {\n console.error('Failed to fetch usage:', response.statusText)\n return\n }\n\n const usage: UsageMetrics = await response.json()\n\n const alerts: string[] = []\n\n for (const [metric, threshold] of Object.entries(THRESHOLDS)) {\n const current = usage[metric as keyof UsageMetrics] as number\n const percent = (current / threshold) * 100\n\n if (percent >= 90) {\n alerts.push(`CRITICAL: ${metric} at ${percent.toFixed(1)}% (${current}/${threshold})`)\n } else if (percent >= 75) {\n alerts.push(`WARNING: ${metric} at ${percent.toFixed(1)}% (${current}/${threshold})`)\n }\n }\n\n if (alerts.length > 0) {\n console.warn('Cost alerts:\\n' + alerts.join('\\n'))\n // Send to Slack, PagerDuty, email, etc.\n await sendCostAlert(alerts)\n } else {\n console.log('All usage metrics within budget')\n }\n}\n\nasync function sendCostAlert(alerts: string[]) {\n // Example: Slack webhook\n const webhookUrl = process.env.SLACK_WEBHOOK_URL\n if (!webhookUrl) return\n\n await fetch(webhookUrl, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n text: `*Supabase Cost Alert* (${PROJECT_REF})\\n${alerts.join('\\n')}`,\n }),\n })\n}\n```\n\n### Security Audit Script\n\n```typescript\n// scripts/supabase-security-audit.ts\nimport { createClient } from '@supabase/supabase-js'\n\nconst supabase = createClient(\n process.env.SUPABASE_URL!,\n process.env.SUPABASE_SERVICE_ROLE_KEY!,\n { auth: { autoRefreshToken: false, persistSession: false } }\n)\n\ninterface AuditFinding {\n severity: 'critical' | 'high' | 'medium' | 'low'\n category: string\n description: string\n remediation: string\n}\n\nexport async function runSecurityAudit(): Promise\u003cAuditFinding[]> {\n const findings: AuditFinding[] = []\n\n // Check 1: Tables without RLS\n const { data: noRls } = await supabase.rpc('run_sql', {\n sql: `\n SELECT tablename FROM pg_tables\n WHERE schemaname = 'public'\n AND rowsecurity = false\n AND tablename NOT LIKE '\\\\_%'\n `,\n })\n\n for (const row of noRls ?? []) {\n findings.push({\n severity: 'critical',\n category: 'RLS',\n description: `Table \"${row.tablename}\" has RLS disabled`,\n remediation: `ALTER TABLE public.${row.tablename} ENABLE ROW LEVEL SECURITY;`,\n })\n }\n\n // Check 2: Tables with RLS enabled but no policies\n const { data: noPolicies } = await supabase.rpc('run_sql', {\n sql: `\n SELECT t.tablename\n FROM pg_tables t\n LEFT JOIN pg_policies p ON p.tablename = t.tablename AND p.schemaname = t.schemaname\n WHERE t.schemaname = 'public'\n AND t.rowsecurity = true\n AND p.policyname IS NULL\n `,\n })\n\n for (const row of noPolicies ?? []) {\n findings.push({\n severity: 'high',\n category: 'RLS',\n description: `Table \"${row.tablename}\" has RLS enabled but no policies (blocks all access)`,\n remediation: 'Add appropriate RLS policies or this table is inaccessible via API',\n })\n }\n\n // Check 3: Overly permissive policies (USING (true) for non-public tables)\n const { data: permissive } = await supabase.rpc('run_sql', {\n sql: `\n SELECT tablename, policyname, qual\n FROM pg_policies\n WHERE schemaname = 'public'\n AND qual = 'true'\n AND cmd != 'SELECT'\n `,\n })\n\n for (const row of permissive ?? []) {\n findings.push({\n severity: 'high',\n category: 'RLS',\n description: `Policy \"${row.policyname}\" on \"${row.tablename}\" allows unrestricted writes (USING true)`,\n remediation: 'Restrict policy to owner or organization scope',\n })\n }\n\n // Check 4: Foreign key columns without indexes\n const { data: missingIdx } = await supabase.rpc('run_sql', {\n sql: `\n SELECT\n tc.table_name,\n kcu.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name\n LEFT JOIN pg_indexes pi\n ON pi.tablename = tc.table_name\n AND pi.indexdef LIKE '%' || kcu.column_name || '%'\n WHERE tc.constraint_type = 'FOREIGN KEY'\n AND tc.table_schema = 'public'\n AND pi.indexname IS NULL\n `,\n })\n\n for (const row of missingIdx ?? []) {\n findings.push({\n severity: 'medium',\n category: 'Performance',\n description: `Foreign key ${row.table_name}.${row.column_name} has no index`,\n remediation: `CREATE INDEX idx_${row.table_name}_${row.column_name} ON public.${row.table_name}(${row.column_name});`,\n })\n }\n\n // Check 5: Storage buckets without RLS\n const { data: buckets } = await supabase.storage.listBuckets()\n for (const bucket of buckets ?? []) {\n if (bucket.public) {\n findings.push({\n severity: 'low',\n category: 'Storage',\n description: `Bucket \"${bucket.name}\" is public — verify this is intentional`,\n remediation: 'Set bucket to private if it contains sensitive files',\n })\n }\n }\n\n return findings\n}\n\n// Run and display results\nasync function main() {\n const findings = await runSecurityAudit()\n\n const critical = findings.filter(f => f.severity === 'critical')\n const high = findings.filter(f => f.severity === 'high')\n\n console.log(`\\nSecurity Audit Results:`)\n console.log(` Critical: ${critical.length}`)\n console.log(` High: ${high.length}`)\n console.log(` Medium: ${findings.filter(f => f.severity === 'medium').length}`)\n console.log(` Low: ${findings.filter(f => f.severity === 'low').length}`)\n\n for (const finding of findings) {\n console.log(`\\n[${finding.severity.toUpperCase()}] ${finding.category}: ${finding.description}`)\n console.log(` Fix: ${finding.remediation}`)\n }\n\n // Exit with error code if critical/high issues found\n if (critical.length > 0 || high.length > 0) {\n process.exit(1)\n }\n}\n\nmain()\n```\n\n### Scheduled Audit via Edge Function\n\n```typescript\n// supabase/functions/security-audit/index.ts\nimport { createClient } from 'https://esm.sh/@supabase/supabase-js@2'\n\nDeno.serve(async () => {\n const supabase = createClient(\n Deno.env.get('SUPABASE_URL')!,\n Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!\n )\n\n // Check tables without RLS\n const { data: noRls } = await supabase\n .from('pg_tables')\n .select('tablename')\n .eq('schemaname', 'public')\n .eq('rowsecurity', false)\n\n const issues = (noRls ?? []).map(t => t.tablename)\n\n if (issues.length > 0) {\n // Store audit result\n await supabase.from('audit_log').insert({\n event: 'security_audit',\n severity: 'critical',\n details: { tables_without_rls: issues },\n })\n }\n\n return new Response(JSON.stringify({\n status: issues.length === 0 ? 'pass' : 'fail',\n tables_without_rls: issues,\n checked_at: new Date().toISOString(),\n }))\n})\n```\n\n## Output\n\n- Shared RLS policy library with owner-only, org-scoped, and public-read templates\n- Naming convention validation function checking tables, columns, FKs, and booleans\n- CI pipeline enforcing RLS, naming, and destructive operation controls\n- Pre-commit hook blocking hardcoded secrets and tables without RLS\n- Cost monitoring script with configurable thresholds and Slack alerting\n- Security audit script detecting missing RLS, permissive policies, and missing indexes\n- Scheduled Edge Function for continuous security monitoring\n\n## Error Handling\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| CI RLS check fails on new table | Migration missing `ENABLE ROW LEVEL SECURITY` | Add `ALTER TABLE` after `CREATE TABLE` in same migration |\n| Naming convention false positive | Table is intentionally singular (e.g., `config`) | Add to exclusion list in validation function |\n| Cost alert not firing | Missing `SUPABASE_ACCESS_TOKEN` | Generate token at supabase.com/dashboard/account/tokens |\n| Security audit times out | Too many tables to scan | Run audit on specific schemas or paginate results |\n| Pre-commit blocks legitimate JWT in test | Test fixture contains JWT-like string | Add test file path to exclusion pattern |\n| RLS template function not found | Migration not applied | Run `supabase db reset` or apply migration manually |\n\n## Examples\n\n### Apply RLS Template to a New Table\n\n```sql\n-- Create the table\nCREATE TABLE public.tasks (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n org_id uuid NOT NULL REFERENCES public.organizations(id),\n title text NOT NULL,\n is_complete boolean DEFAULT false,\n created_by uuid REFERENCES auth.users(id),\n created_at timestamptz DEFAULT now()\n);\n\n-- Apply org-scoped RLS template (with delete for admins)\nSELECT public.rls_org_scoped('tasks', 'org_id', true);\n\n-- Create index on foreign key\nCREATE INDEX idx_tasks_org_id ON public.tasks(org_id);\n```\n\n### Run Security Audit Locally\n\n```bash\nnpx tsx scripts/supabase-security-audit.ts\n```\n\n### Check Naming Conventions\n\n```sql\nSELECT * FROM public.validate_naming_conventions();\n```\n\n## Resources\n\n- [Supabase Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)\n- [Supabase CLI Migrations](https://supabase.com/docs/guides/cli/managing-environments)\n- [Supabase Management API](https://supabase.com/docs/reference/api/introduction)\n- [Supabase Pricing](https://supabase.com/pricing)\n- [PostgreSQL Naming Conventions](https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS)\n\n## Next Steps\n\nFor architecture patterns across different app types, see `supabase-architecture-variants`.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":15398,"content_sha256":"bff9f5c833995f435cba81462fa64f763958828105fb9b32df30fa342d0280aa"},{"filename":"references/errors.md","content":"# Error Handling Reference\n\n| Issue | Cause | Solution |\n|-------|-------|----------|\n| ESLint rule not firing | Wrong config | Check plugin registration |\n| Pre-commit skipped | --no-verify | Enforce in CI |\n| Policy false positive | Regex too broad | Narrow pattern match |\n| Guardrail triggered | Actual issue | Fix or whitelist |\n\n---\n*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":487,"content_sha256":"0eb4052e5404d2c62c81b86f05abaaeb54935a37283d6ff9afb7380e448ab763"},{"filename":"references/eslint-rules.md","content":"# Eslint Rules\n\n## ESLint Rules\n\n### Custom Supabase Plugin\n\n```javascript\n// eslint-plugin-supabase/rules/no-hardcoded-keys.js\nmodule.exports = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Disallow hardcoded Supabase API keys',\n },\n fixable: 'code',\n },\n create(context) {\n return {\n Literal(node) {\n if (typeof node.value === 'string') {\n if (node.value.match(/^sk_(live|test)_[a-zA-Z0-9]{24,}/)) {\n context.report({\n node,\n message: 'Hardcoded Supabase API key detected',\n });\n }\n }\n },\n };\n },\n};\n```\n\n### ESLint Configuration\n\n```javascript\n// .eslintrc.js\nmodule.exports = {\n plugins: ['supabase'],\n rules: {\n 'supabase/no-hardcoded-keys': 'error',\n 'supabase/require-error-handling': 'warn',\n 'supabase/use-typed-client': 'warn',\n },\n};\n```\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":885,"content_sha256":"c112b90aa5c97f74221e672587d828e823236df6e62ffb47186b600818c34a7e"},{"filename":"references/examples.md","content":"## Examples\n\n### Quick ESLint Check\n\n```bash\nnpx eslint --plugin supabase --rule 'supabase/no-hardcoded-keys: error' src/\n```\n\n---\n*[Tons of Skills](https://tonsofskills.com) by [Intent Solutions](https://intentsolutions.io) | [jeremylongshore.com](https://jeremylongshore.com)*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":279,"content_sha256":"2e13079c2ce49fee4cba0060a7b3c7133dd53c62b5389541bee1f3e4823a5b40"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Supabase Policy Guardrails","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Organizational governance for Supabase at scale: a ","type":"text"},{"text":"shared RLS policy library","type":"text","marks":[{"type":"strong"}]},{"text":" (reusable templates for common access patterns), ","type":"text"},{"text":"naming conventions","type":"text","marks":[{"type":"strong"}]},{"text":" (tables, columns, functions, policies), ","type":"text"},{"text":"migration review process","type":"text","marks":[{"type":"strong"}]},{"text":" (CI checks ensuring RLS, preventing destructive operations, enforcing naming), ","type":"text"},{"text":"cost alert configuration","type":"text","marks":[{"type":"strong"}]},{"text":" (billing thresholds and usage monitoring), and ","type":"text"},{"text":"security audit scripts","type":"text","marks":[{"type":"strong"}]},{"text":" (scanning for exposed keys, missing RLS, overly permissive policies). All patterns use real ","type":"text"},{"text":"createClient","type":"text","marks":[{"type":"code_inline"}]},{"text":" from ","type":"text"},{"text":"@supabase/supabase-js","type":"text","marks":[{"type":"code_inline"}]},{"text":" and Supabase CLI commands.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Supabase project with ","type":"text"},{"text":"supabase","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI installed and linked","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"@supabase/supabase-js","type":"text","marks":[{"type":"code_inline"}]},{"text":" v2+ installed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CI/CD pipeline (GitHub Actions recommended)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Database access via ","type":"text"},{"text":"psql","type":"text","marks":[{"type":"code_inline"}]},{"text":" or Supabase SQL Editor","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pro plan recommended for cost alerts and usage API","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 1 — Shared RLS Policy Library and Naming Conventions","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"RLS Policy Templates","type":"text"}]},{"type":"paragraph","content":[{"text":"Create reusable RLS policy templates that teams apply to new tables. This prevents each developer from writing ad-hoc policies and ensures consistent access control.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"sql"},"content":[{"text":"-- supabase/migrations/00000000000000_rls_policy_library.sql\n-- Shared RLS policy library — apply these templates to new tables\n\n-- ============================================================\n-- Template 1: Owner-only access (user owns the row)\n-- Usage: tables with a user_id column (todos, profiles, settings)\n-- ============================================================\nCREATE OR REPLACE FUNCTION public.rls_owner_only(table_name text, user_column text DEFAULT 'user_id')\nRETURNS void AS $\nBEGIN\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);\n\n EXECUTE format(\n 'CREATE POLICY \"owner_select\" ON public.%I FOR SELECT USING (%I = auth.uid())',\n table_name, user_column\n );\n EXECUTE format(\n 'CREATE POLICY \"owner_insert\" ON public.%I FOR INSERT WITH CHECK (%I = auth.uid())',\n table_name, user_column\n );\n EXECUTE format(\n 'CREATE POLICY \"owner_update\" ON public.%I FOR UPDATE USING (%I = auth.uid())',\n table_name, user_column\n );\n EXECUTE format(\n 'CREATE POLICY \"owner_delete\" ON public.%I FOR DELETE USING (%I = auth.uid())',\n table_name, user_column\n );\nEND;\n$ LANGUAGE plpgsql;\n\n-- ============================================================\n-- Template 2: Organization-scoped access (user is member of org)\n-- Usage: tables with org_id referencing org_members\n-- ============================================================\nCREATE OR REPLACE FUNCTION public.rls_org_scoped(\n table_name text,\n org_column text DEFAULT 'org_id',\n allow_delete boolean DEFAULT false\n)\nRETURNS void AS $\nBEGIN\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);\n\n EXECUTE format(\n 'CREATE POLICY \"org_select\" ON public.%I FOR SELECT USING (\n %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid())\n )', table_name, org_column\n );\n EXECUTE format(\n 'CREATE POLICY \"org_insert\" ON public.%I FOR INSERT WITH CHECK (\n %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid())\n )', table_name, org_column\n );\n EXECUTE format(\n 'CREATE POLICY \"org_update\" ON public.%I FOR UPDATE USING (\n %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid() AND role IN (''admin'', ''editor''))\n )', table_name, org_column\n );\n\n IF allow_delete THEN\n EXECUTE format(\n 'CREATE POLICY \"org_delete\" ON public.%I FOR DELETE USING (\n %I IN (SELECT org_id FROM public.org_members WHERE user_id = auth.uid() AND role = ''admin'')\n )', table_name, org_column\n );\n END IF;\nEND;\n$ LANGUAGE plpgsql;\n\n-- ============================================================\n-- Template 3: Public read, authenticated write\n-- Usage: blog posts, product listings, public content\n-- ============================================================\nCREATE OR REPLACE FUNCTION public.rls_public_read_auth_write(\n table_name text,\n owner_column text DEFAULT 'created_by'\n)\nRETURNS void AS $\nBEGIN\n EXECUTE format('ALTER TABLE public.%I ENABLE ROW LEVEL SECURITY', table_name);\n\n EXECUTE format(\n 'CREATE POLICY \"public_select\" ON public.%I FOR SELECT USING (true)',\n table_name\n );\n EXECUTE format(\n 'CREATE POLICY \"auth_insert\" ON public.%I FOR INSERT WITH CHECK (auth.uid() IS NOT NULL)',\n table_name\n );\n EXECUTE format(\n 'CREATE POLICY \"owner_update\" ON public.%I FOR UPDATE USING (%I = auth.uid())',\n table_name, owner_column\n );\n EXECUTE format(\n 'CREATE POLICY \"owner_delete\" ON public.%I FOR DELETE USING (%I = auth.uid())',\n table_name, owner_column\n );\nEND;\n$ LANGUAGE plpgsql;\n\n-- Apply templates to tables:\n-- SELECT public.rls_owner_only('todos');\n-- SELECT public.rls_org_scoped('projects', 'org_id', true);\n-- SELECT public.rls_public_read_auth_write('blog_posts', 'author_id');","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Naming Conventions","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"sql"},"content":[{"text":"-- supabase/migrations/00000000000001_naming_convention_check.sql\n-- Validation function that checks naming conventions at migration time\n\nCREATE OR REPLACE FUNCTION public.validate_naming_conventions()\nRETURNS TABLE(issue text, object_name text, suggestion text) AS $\nBEGIN\n -- Tables must be snake_case, plural\n RETURN QUERY\n SELECT\n 'Table name should be plural snake_case'::text,\n t.tablename::text,\n regexp_replace(t.tablename, '([A-Z])', '_\\1', 'g')::text\n FROM pg_tables t\n WHERE t.schemaname = 'public'\n AND (\n t.tablename ~ '[A-Z]' -- contains uppercase\n OR t.tablename ~ '-' -- contains hyphens\n OR t.tablename !~ 's

Supabase Policy Guardrails Overview Organizational governance for Supabase at scale: a shared RLS policy library (reusable templates for common access patterns), naming conventions (tables, columns, functions, policies), migration review process (CI checks ensuring RLS, preventing destructive operations, enforcing naming), cost alert configuration (billing thresholds and usage monitoring), and security audit scripts (scanning for exposed keys, missing RLS, overly permissive policies). All patterns use real from and Supabase CLI commands. Prerequisites - Supabase project with CLI installed and…

-- not plural (heuristic)\n )\n AND t.tablename NOT LIKE '\\_%'; -- skip internal tables\n\n -- Columns must be snake_case\n RETURN QUERY\n SELECT\n 'Column name should be snake_case'::text,\n (c.table_name || '.' || c.column_name)::text,\n regexp_replace(c.column_name, '([A-Z])', '_\\1', 'g')::text\n FROM information_schema.columns c\n WHERE c.table_schema = 'public'\n AND (c.column_name ~ '[A-Z]' OR c.column_name ~ '-');\n\n -- Foreign key columns should end with _id\n RETURN QUERY\n SELECT\n 'Foreign key column should end with _id'::text,\n (tc.table_name || '.' || kcu.column_name)::text,\n (kcu.column_name || '_id')::text\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name\n WHERE tc.constraint_type = 'FOREIGN KEY'\n AND tc.table_schema = 'public'\n AND kcu.column_name NOT LIKE '%_id';\n\n -- Boolean columns should start with is_ or has_\n RETURN QUERY\n SELECT\n 'Boolean column should start with is_ or has_'::text,\n (c.table_name || '.' || c.column_name)::text,\n ('is_' || c.column_name)::text\n FROM information_schema.columns c\n WHERE c.table_schema = 'public'\n AND c.data_type = 'boolean'\n AND c.column_name NOT LIKE 'is_%'\n AND c.column_name NOT LIKE 'has_%';\nEND;\n$ LANGUAGE plpgsql;\n\n-- Run: SELECT * FROM public.validate_naming_conventions();","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Naming Convention Reference","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Object","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Convention","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Example","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tables","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plural snake_case","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"user_profiles","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"order_items","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Columns","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"snake_case","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"created_at","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"full_name","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Foreign keys","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{referenced_table_singular}_id","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"user_id","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"order_id","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Booleans","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"is_","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"has_","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"is_active","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"has_verified_email","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Timestamps","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"_at","type":"text","marks":[{"type":"code_inline"}]},{"text":" suffix","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"created_at","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"updated_at","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"deleted_at","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RLS policies","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{scope}_{operation}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"owner_select","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"org_insert","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Functions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"verb_noun","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"create_user","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"get_dashboard_metrics","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Indexes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"idx_{table}_{columns}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"idx_orders_user_id_created_at","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migrations","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"{timestamp}_{verb}_{description}","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"20250322000000_create_orders_table.sql","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Step 2 — Migration Review Process with CI Checks","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"CI checks, cost alerts, and security audits","type":"text","marks":[{"type":"link","attrs":{"href":"references/ci-cost-security.md","title":null}}]},{"text":" for GitHub Actions migration guardrails (RLS enforcement, naming checks, destructive operation blocks), pre-commit hooks, cost monitoring with Slack alerts, security audit scripts, and scheduled Edge Function audits.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Shared RLS policy library with owner-only, org-scoped, and public-read templates","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Naming convention validation function checking tables, columns, FKs, and booleans","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CI pipeline enforcing RLS, naming, and destructive operation controls","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pre-commit hook blocking hardcoded secrets and tables without RLS","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cost monitoring script with configurable thresholds and Slack alerting","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security audit script detecting missing RLS, permissive policies, and missing indexes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Scheduled Edge Function for continuous security monitoring","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Handling","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Issue","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Solution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CI RLS check fails on new table","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migration missing ","type":"text"},{"text":"ENABLE ROW LEVEL SECURITY","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"ALTER TABLE","type":"text","marks":[{"type":"code_inline"}]},{"text":" after ","type":"text"},{"text":"CREATE TABLE","type":"text","marks":[{"type":"code_inline"}]},{"text":" in same migration","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Naming convention false positive","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Table is intentionally singular (e.g., ","type":"text"},{"text":"config","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add to exclusion list in validation function","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cost alert not firing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing ","type":"text"},{"text":"SUPABASE_ACCESS_TOKEN","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate token at supabase.com/dashboard/account/tokens","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Security audit times out","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Too many tables to scan","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run audit on specific schemas or paginate results","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pre-commit blocks legitimate JWT in test","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Test fixture contains JWT-like string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add test file path to exclusion pattern","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"RLS template function not found","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Migration not applied","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"supabase db reset","type":"text","marks":[{"type":"code_inline"}]},{"text":" or apply migration manually","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Examples","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"CI, cost, and security reference","type":"text","marks":[{"type":"link","attrs":{"href":"references/ci-cost-security.md","title":null}}]},{"text":" for full examples including applying RLS templates, running security audits, and checking naming conventions.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Resources","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Supabase Row Level Security","type":"text","marks":[{"type":"link","attrs":{"href":"https://supabase.com/docs/guides/database/postgres/row-level-security","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Supabase CLI Migrations","type":"text","marks":[{"type":"link","attrs":{"href":"https://supabase.com/docs/guides/cli/managing-environments","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Supabase Management API","type":"text","marks":[{"type":"link","attrs":{"href":"https://supabase.com/docs/reference/api/introduction","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Supabase Pricing","type":"text","marks":[{"type":"link","attrs":{"href":"https://supabase.com/pricing","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PostgreSQL Naming Conventions","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS","title":null}}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Next Steps","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"For architecture patterns across different app types, see ","type":"text"},{"text":"supabase-architecture-variants","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"supabase-policy-guardrails","tags":["saas","supabase","governance","security","rls","naming-conventions","cost-management"],"author":"@skillopedia","source":{"stars":2275,"repo_name":"claude-code-plugins-plus-skills","origin_url":"https://github.com/jeremylongshore/claude-code-plugins-plus-skills/blob/HEAD/plugins/saas-packs/supabase-pack/skills/supabase-policy-guardrails/SKILL.md","repo_owner":"jeremylongshore","body_sha256":"897f4d8d49a8fa9eb3a8db1855802f806783e18ce1d48a0a64eb01001da761e4","cluster_key":"d18d2b4ebd0383f5db834660e4503e878adaeaa21b3c56cb89c34bd801eb3e37","clean_bundle":{"format":"clean-skill-bundle-v1","source":"jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/supabase-pack/skills/supabase-policy-guardrails/SKILL.md","attachments":[{"id":"91772090-ef5b-526d-8d8b-6b645380f73b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/91772090-ef5b-526d-8d8b-6b645380f73b/attachment.md","path":"references/ci-cost-security.md","size":15398,"sha256":"bff9f5c833995f435cba81462fa64f763958828105fb9b32df30fa342d0280aa","contentType":"text/markdown; charset=utf-8"},{"id":"ce8352ec-3442-5877-9016-71facb2a651b","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ce8352ec-3442-5877-9016-71facb2a651b/attachment.md","path":"references/errors.md","size":487,"sha256":"0eb4052e5404d2c62c81b86f05abaaeb54935a37283d6ff9afb7380e448ab763","contentType":"text/markdown; charset=utf-8"},{"id":"5217ec01-8c1a-546f-aefb-8152751c07fd","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5217ec01-8c1a-546f-aefb-8152751c07fd/attachment.md","path":"references/eslint-rules.md","size":885,"sha256":"c112b90aa5c97f74221e672587d828e823236df6e62ffb47186b600818c34a7e","contentType":"text/markdown; charset=utf-8"},{"id":"403a40be-cc18-504b-b2a7-d3c0d2deff11","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/403a40be-cc18-504b-b2a7-d3c0d2deff11/attachment.md","path":"references/examples.md","size":279,"sha256":"2e13079c2ce49fee4cba0060a7b3c7133dd53c62b5389541bee1f3e4823a5b40","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"46bea9e5ff63beef46aacec5384b3f52101d93fd7301d26b055919a8249cd775","attachment_count":4,"text_attachments":4,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"plugins/saas-packs/supabase-pack/skills/supabase-policy-guardrails/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"license":"MIT","version":"v1","category":"security","import_tag":"clean-skills-v1","description":"Enforce organizational governance for Supabase projects: shared RLS policy\nlibrary with reusable templates, table and column naming conventions,\nmigration review process with CI checks, cost alert thresholds,\nand security audit scripts scanning for common misconfigurations.\nUse when establishing Supabase standards across teams, creating RLS\npolicy templates, setting up migration review workflows, or auditing\nexisting projects for security and cost issues.\nTrigger with phrases like \"supabase governance\", \"supabase policy library\",\n\"supabase naming convention\", \"supabase migration review\",\n\"supabase cost alert\", \"supabase security audit\", \"supabase RLS template\".\n","allowed-tools":"Read, Write, Edit, Bash(supabase:*), Bash(psql:*), Bash(npx:*), Grep","compatibility":"Designed for Claude Code, also compatible with Codex and OpenClaw"}},"renderedAt":1782981791998}

Supabase Policy Guardrails Overview Organizational governance for Supabase at scale: a shared RLS policy library (reusable templates for common access patterns), naming conventions (tables, columns, functions, policies), migration review process (CI checks ensuring RLS, preventing destructive operations, enforcing naming), cost alert configuration (billing thresholds and usage monitoring), and security audit scripts (scanning for exposed keys, missing RLS, overly permissive policies). All patterns use real from and Supabase CLI commands. Prerequisites - Supabase project with CLI installed and…