skills/ballee/rls-policy-generator/SKILL.md
Generate production-ready Row Level Security (RLS) policies for Supabase tables with is_super_admin() bypass, proper USING/WITH CHECK, and common relationship patterns; use when creating RLS policies, securing tables, implementing data access controls, or securing storage buckets
npx skillsauth add javeedishaq/ai-workflow-orchestrator rls-policy-generatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
This skill has been flagged as suspicious. Review the scan results before using.
2 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Environment | Project ID | Dashboard SQL Editor |
|-------------|-----------|---------------------|
| Production | csjruhqyqzzqxnfeyiaf | SQL Editor |
| Staging | hxpcknyqswetsqmqmeep | SQL Editor |
Credentials are stored in .env.local:
# Load credentials
source .env.local
# Production
PGPASSWORD="${PROD_SUPABASE_DB_PASSWORD}" psql "postgresql://postgres.csjruhqyqzzqxnfeyiaf@aws-1-eu-central-1.pooler.supabase.com:5432/postgres"
# Staging
PGPASSWORD="${STAGING_SUPABASE_DB_PASSWORD}" psql "postgresql://postgres.hxpcknyqswetsqmqmeep@aws-1-eu-central-1.pooler.supabase.com:5432/postgres"
Use this skill when:
DO NOT use for:
This is the #1 source of RLS bugs in Ballee. Understand this before writing any account-related policies.
Ballee uses MakerKit's dual-account system:
| Account Type | Count (Dec 2025) | Membership Record | Ownership Check |
|--------------|------------------|-------------------|-----------------|
| Personal | 94 dancers | ❌ NO accounts_memberships record | accounts.primary_owner_user_id = auth.uid() |
| Team | 1 (admin team) | ✅ Has accounts_memberships record | accounts_memberships.user_id = auth.uid() |
By design in MakerKit, when a user signs up:
kit.setup_new_user() trigger creates a personal accountprimary_owner_user_idadd_current_user_to_new_account trigger only fires for team accounts (is_personal_account = false)-- ❌ BROKEN: Only checks accounts_memberships (misses 100% of dancers!)
CREATE POLICY broken_policy ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
(storage.foldername(name))[1] IN (
SELECT account_id::text FROM accounts_memberships
WHERE user_id = auth.uid()
)
);
-- Result: All 94 dancers get "permission denied"
-- ✅ CORRECT: Checks BOTH team memberships AND personal account ownership
CREATE POLICY correct_policy ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
-- Super admin bypass
public.is_super_admin() OR
-- Team account: user has membership
(storage.foldername(name))[1] IN (
SELECT account_id::text FROM accounts_memberships
WHERE user_id = auth.uid()
) OR
-- Personal account: user is the primary owner
(storage.foldername(name))[1] IN (
SELECT id::text FROM accounts
WHERE primary_owner_user_id = auth.uid()
AND is_personal_account = true
)
);
accounts_memberships⚠️ Every time you write a policy that queries accounts_memberships, ask:
OR primary_owner_user_id = auth.uid() check?AND is_personal_account = true?| Table | Pattern Used | Notes |
|-------|-------------|-------|
| notifications | ✅ Both checks | Has primary_owner_user_id OR accounts_memberships |
| storage.objects (reimbursements) | ✅ Both checks | Fixed Dec 2025 |
These tables don't need the dual check because they use user_id = auth.uid():
| Table | Column | Notes |
|-------|--------|-------|
| profiles | id = auth.uid() | Profile ID = User ID |
| professional_profiles | user_id = auth.uid() | Direct user ownership |
| reimbursement_requests | user_id = auth.uid() | Direct user ownership |
| dancer_profiles | user_id = auth.uid() | Direct user ownership |
-- Pattern 1: User owns the record directly (SAFEST - no account check needed)
USING (user_id = auth.uid())
-- Pattern 2: Account-based with BOTH personal and team support
USING (
public.is_super_admin() OR
account_id IN (
-- Team accounts via membership
SELECT account_id FROM accounts_memberships WHERE user_id = auth.uid()
UNION
-- Personal accounts via ownership
SELECT id FROM accounts WHERE primary_owner_user_id = auth.uid() AND is_personal_account = true
)
)
-- Pattern 3: Account-based for storage (path-based)
USING (
public.is_super_admin() OR
-- Team account path
(storage.foldername(name))[1] IN (
SELECT account_id::text FROM accounts_memberships WHERE user_id = auth.uid()
) OR
-- Personal account path
(storage.foldername(name))[1] IN (
SELECT id::text FROM accounts WHERE primary_owner_user_id = auth.uid() AND is_personal_account = true
)
)
The #1 cause of production outages with RLS is infinite recursion.
RLS policies that query tables which have RLS policies that query back to the original table:
-- ❌ DANGEROUS: productions policy queries cast_assignments
CREATE POLICY productions_select ON public.productions
FOR SELECT TO authenticated
USING (
EXISTS (
SELECT 1 FROM cast_assignments ca
JOIN cast_roles cr ON ca.cast_role_id = cr.id
WHERE cr.production_id = productions.id -- References back to productions!
AND ca.user_id = auth.uid()
)
);
-- When you SELECT from productions:
-- 1. PostgreSQL checks productions RLS → queries cast_assignments
-- 2. cast_assignments might have RLS → queries cast_roles
-- 3. cast_roles RLS → queries productions (RECURSION!)
Pattern 1: Denormalized Arrays (RECOMMENDED)
-- Store user IDs directly on the table
ALTER TABLE productions ADD COLUMN assigned_user_ids uuid[] DEFAULT '{}';
-- Policy uses local array - no joins needed
CREATE POLICY productions_select ON public.productions
FOR SELECT TO authenticated
USING (
is_super_admin() OR
auth.uid() = ANY(assigned_user_ids) -- No subquery!
);
Pattern 2: service_role Bypass
-- service_role always bypasses RLS
CREATE POLICY productions_service_role_bypass ON public.productions
FOR ALL TO service_role
USING (true) WITH CHECK (true);
Pattern 3: Direct Column Check
-- Use columns directly on the table, not JOINs
CREATE POLICY productions_select ON public.productions
FOR SELECT TO authenticated
USING (
is_super_admin() OR
created_by = auth.uid() OR
client_id IN (SELECT client_id FROM client_users WHERE user_id = auth.uid())
);
-- Check for circular policy dependencies
SELECT tablename, policyname, qual
FROM pg_policies
WHERE schemaname = 'public'
AND qual LIKE '%EXISTS%'
ORDER BY tablename;
-- Identify tables referenced in a policy
SELECT tablename, policyname,
regexp_matches(qual, 'FROM\s+(\w+)', 'g') as referenced_tables
FROM pg_policies
WHERE schemaname = 'public';
# Get policy lists
PGPASSWORD="${STAGING_SUPABASE_DB_PASSWORD}" psql "postgresql://postgres.hxpcknyqswetsqmqmeep@aws-1-eu-central-1.pooler.supabase.com:5432/postgres" -t -c "SELECT tablename || '.' || policyname FROM pg_policies WHERE schemaname = 'public' ORDER BY 1;" > /tmp/staging.txt
PGPASSWORD="${PROD_SUPABASE_DB_PASSWORD}" psql "postgresql://postgres.csjruhqyqzzqxnfeyiaf@aws-1-eu-central-1.pooler.supabase.com:5432/postgres" -t -c "SELECT tablename || '.' || policyname FROM pg_policies WHERE schemaname = 'public' ORDER BY 1;" > /tmp/prod.txt
# Compare
sort /tmp/staging.txt | uniq > /tmp/s.txt
sort /tmp/prod.txt | uniq > /tmp/p.txt
echo "Missing in production:" && comm -23 /tmp/s.txt /tmp/p.txt
echo "Extra in production:" && comm -13 /tmp/s.txt /tmp/p.txt
-- Get policy definition from staging
SELECT
'DROP POLICY IF EXISTS "' || policyname || '" ON ' || schemaname || '.' || tablename || ';' ||
E'\nCREATE POLICY "' || policyname || '" ON ' || schemaname || '.' || tablename ||
' FOR ' || cmd || ' TO ' ||
CASE WHEN roles = '{authenticated}' THEN 'authenticated'
WHEN roles = '{service_role}' THEN 'service_role'
ELSE array_to_string(roles, ', ')
END ||
CASE WHEN cmd IN ('SELECT', 'DELETE') THEN ' USING (' || qual || ');'
WHEN cmd = 'INSERT' THEN ' WITH CHECK (' || with_check || ');'
ELSE ' USING (' || COALESCE(qual, 'true') || ') WITH CHECK (' || COALESCE(with_check, 'true') || ');'
END as sql
FROM pg_policies
WHERE tablename = 'TABLE_NAME' AND policyname = 'POLICY_NAME';
for table in productions events cast_roles cast_assignments; do
echo "=== $table ==="
echo "STAGING:"
PGPASSWORD="${STAGING_SUPABASE_DB_PASSWORD}" psql "..." -t -c "SELECT COUNT(*) FROM pg_policies WHERE tablename = '$table';"
echo "PRODUCTION:"
PGPASSWORD="${PROD_SUPABASE_DB_PASSWORD}" psql "..." -t -c "SELECT COUNT(*) FROM pg_policies WHERE tablename = '$table';"
done
-- CRITICAL: Find policies that allow public write access
SELECT tablename, policyname, cmd, roles
FROM pg_policies
WHERE schemaname = 'public'
AND qual = 'true'
AND (roles @> '{public}' OR roles @> '{anon}')
AND cmd != 'SELECT'
ORDER BY tablename;
-- Find "Allow all" policies (should be removed!)
SELECT tablename, policyname, cmd, roles
FROM pg_policies
WHERE policyname LIKE 'Allow all%'
ORDER BY tablename;
-- Tables with multiple policies for same operation
SELECT tablename, cmd, COUNT(*) as policy_count,
string_agg(policyname, ', ' ORDER BY policyname) as policies
FROM pg_policies
WHERE schemaname = 'public'
GROUP BY tablename, cmd
HAVING COUNT(*) > 1
ORDER BY policy_count DESC;
-- Check which tables have service_role bypass
SELECT tablename, policyname
FROM pg_policies
WHERE schemaname = 'public'
AND policyname LIKE '%service_role%'
ORDER BY tablename;
-- Tables WITHOUT service_role bypass (may need one)
SELECT DISTINCT tablename
FROM pg_policies
WHERE schemaname = 'public'
AND tablename NOT IN (
SELECT DISTINCT tablename
FROM pg_policies
WHERE policyname LIKE '%service_role%'
)
ORDER BY tablename;
ALTER TABLE "public"."table_name" ENABLE ROW LEVEL SECURITY;
-- service_role is the backend/admin client - needs unrestricted access
-- @rls-disable-check: service_role bypass is required for admin operations
CREATE POLICY {table}_service_role_bypass ON public.{table}
FOR ALL TO service_role
USING (true) WITH CHECK (true);
-- ALWAYS include is_super_admin() first in USING/WITH CHECK
USING (
public.is_super_admin() OR
-- other conditions
)
DROP POLICY IF EXISTS policy_name ON public.table_name;
CREATE POLICY policy_name ON public.table_name ...
Standard pattern: {table}_{operation} or {table}_{role}_{operation}
| Pattern | Example | Use When |
|---------|---------|----------|
| {table}_{operation} | events_select | Default for all policies |
| {table}_{role}_{operation} | hire_orders_dancer_read | Role-specific access |
| {table}_service_role_bypass | productions_service_role_bypass | service_role ALL access |
| {table}_{operation}_by_{entity} | events_insert_by_client | Entity-based access |
AVOID:
_restricted suffix: ❌ events_select_restricted (use _by_role instead)_select and _select_all-- Enable RLS
ALTER TABLE "public".{table_name} ENABLE ROW LEVEL SECURITY;
-- service_role bypass (for backend operations)
DROP POLICY IF EXISTS {table_name}_service_role_bypass ON public.{table_name};
CREATE POLICY {table_name}_service_role_bypass ON public.{table_name}
FOR ALL TO service_role
USING (true) WITH CHECK (true);
-- Super admin operations
DROP POLICY IF EXISTS {table_name}_select ON public.{table_name};
CREATE POLICY {table_name}_select ON public.{table_name}
FOR SELECT TO authenticated
USING (public.is_super_admin());
DROP POLICY IF EXISTS {table_name}_insert ON public.{table_name};
CREATE POLICY {table_name}_insert ON public.{table_name}
FOR INSERT TO authenticated
WITH CHECK (public.is_super_admin());
DROP POLICY IF EXISTS {table_name}_update ON public.{table_name};
CREATE POLICY {table_name}_update ON public.{table_name}
FOR UPDATE TO authenticated
USING (public.is_super_admin())
WITH CHECK (public.is_super_admin());
DROP POLICY IF EXISTS {table_name}_delete ON public.{table_name};
CREATE POLICY {table_name}_delete ON public.{table_name}
FOR DELETE TO authenticated
USING (public.is_super_admin());
-- SELECT: User sees their own + super admin sees all
DROP POLICY IF EXISTS {table_name}_select ON public.{table_name};
CREATE POLICY {table_name}_select ON public.{table_name}
FOR SELECT TO authenticated
USING (
public.is_super_admin() OR
user_id = auth.uid()
);
-- INSERT: User can insert their own
DROP POLICY IF EXISTS {table_name}_insert ON public.{table_name};
CREATE POLICY {table_name}_insert ON public.{table_name}
FOR INSERT TO authenticated
WITH CHECK (
public.is_super_admin() OR
user_id = auth.uid()
);
-- UPDATE: User can update their own
DROP POLICY IF EXISTS {table_name}_update ON public.{table_name};
CREATE POLICY {table_name}_update ON public.{table_name}
FOR UPDATE TO authenticated
USING (public.is_super_admin() OR user_id = auth.uid())
WITH CHECK (public.is_super_admin() OR user_id = auth.uid());
-- DELETE: Super admin only (or user owns)
DROP POLICY IF EXISTS {table_name}_delete ON public.{table_name};
CREATE POLICY {table_name}_delete ON public.{table_name}
FOR DELETE TO authenticated
USING (public.is_super_admin() OR user_id = auth.uid());
-- Use when access is determined by assignment/membership
-- Store user IDs in an array column on the table
-- SELECT: User is in assigned_user_ids array
DROP POLICY IF EXISTS {table_name}_select ON public.{table_name};
CREATE POLICY {table_name}_select ON public.{table_name}
FOR SELECT TO authenticated
USING (
public.is_super_admin() OR
auth.uid() = ANY(assigned_user_ids)
);
-- For tables owned by clients (Fever, etc.)
DROP POLICY IF EXISTS {table_name}_select_by_client ON public.{table_name};
CREATE POLICY {table_name}_select_by_client ON public.{table_name}
FOR SELECT TO authenticated
USING (
public.is_super_admin() OR
client_id IN (
SELECT client_id FROM client_users WHERE user_id = auth.uid()
)
);
-- View all policies for a table
SELECT policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE tablename = 'TABLE_NAME'
ORDER BY cmd, policyname;
-- View policy count per table
SELECT tablename, COUNT(*) as policy_count
FROM pg_policies
WHERE schemaname = 'public'
GROUP BY tablename
ORDER BY policy_count DESC;
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'TABLE_NAME';
| Error | Cause | Fix | |-------|-------|-----| | "infinite recursion detected" | Policy queries table with RLS that queries back | Use denormalized arrays or service_role bypass | | "permission denied for table" | RLS blocking access | Check is_super_admin() bypass exists | | "policy does not exist" | Typo or not created | Run DROP IF EXISTS + CREATE | | Empty results for super admin | is_super_admin() not first in USING | Move is_super_admin() OR to start |
Storage buckets in Supabase require RLS policies on storage.objects. These are different from table RLS - they control file access, upload, and deletion.
dancer-media, venue-documents)Use centralized constants from @kit/shared/storage:
import { StorageBuckets, SignedUrlExpiry } from '@kit/shared/storage';
// Available buckets
StorageBuckets.DANCER_MEDIA // 'dancer-media'
StorageBuckets.VENUE_DOCUMENTS // 'venue-documents'
StorageBuckets.LEGAL_DOCUMENTS // 'legal-documents'
StorageBuckets.REIMBURSEMENT_DOCUMENTS // 'reimbursement-documents'
StorageBuckets.REIMBURSEMENTS // 'reimbursements' (legacy)
StorageBuckets.CONTRACTS // 'contracts'
StorageBuckets.PRODUCTION_DOCUMENTS // 'production-documents'
StorageBuckets.INVOICE_PDFS // 'invoice-pdfs'
// Expiry times (in seconds)
SignedUrlExpiry.IMMEDIATE_DISPLAY // 3600 (1 hour)
SignedUrlExpiry.DOWNLOAD // 86400 (24 hours)
SignedUrlExpiry.PROFILE_PHOTO // 604800 (7 days)
SignedUrlExpiry.ADMIN_REVIEW // 86400 (24 hours)
SignedUrlExpiry.MAX // 604800 (7 days - Supabase limit)
SignedUrlExpiry.MIN // 60 (1 minute)
-- Create a private bucket (requires signed URLs)
INSERT INTO storage.buckets (id, name, public)
VALUES ('my-bucket', 'my-bucket', false);
-- Create a public bucket (anyone can read)
INSERT INTO storage.buckets (id, name, public)
VALUES ('public-assets', 'public-assets', true);
Use when files are named with account UUIDs (e.g., profile photos):
-- Uses kit.get_storage_filename_as_uuid() to extract UUID from filename
CREATE POLICY account_image ON storage.objects FOR ALL USING (
bucket_id = 'account_image'
AND (
-- Super admin can access all files
public.is_super_admin()
OR
-- File belongs to user's account
kit.get_storage_filename_as_uuid(name) = auth.uid()
OR
-- User has role on the account
public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
WITH CHECK (
bucket_id = 'account_image'
AND (
-- Super admin can upload/modify
public.is_super_admin()
OR
-- User owns the account
kit.get_storage_filename_as_uuid(name) = auth.uid()
OR
-- User has permission on the account
public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'settings.manage'
)
)
);
Use when file paths include user ID (e.g., {user_id}/photos/image.jpg):
-- Extract user_id from the first path segment
CREATE POLICY dancer_media ON storage.objects FOR ALL USING (
bucket_id = 'dancer-media'
AND (
public.is_super_admin()
OR
-- First segment of path is user's UUID
(string_to_array(name, '/'))[1]::uuid = auth.uid()
)
)
WITH CHECK (
bucket_id = 'dancer-media'
AND (
public.is_super_admin()
OR
(string_to_array(name, '/'))[1]::uuid = auth.uid()
)
);
Use when files are linked to database records via foreign key:
-- Example: venue documents linked to venues table
CREATE POLICY venue_documents ON storage.objects FOR ALL USING (
bucket_id = 'venue-documents'
AND (
public.is_super_admin()
OR
-- Extract venue_id from path and check access
EXISTS (
SELECT 1 FROM venues v
WHERE v.id = (string_to_array(name, '/'))[1]::uuid
AND public.has_role_on_account(v.account_id)
)
)
)
WITH CHECK (
bucket_id = 'venue-documents'
AND (
public.is_super_admin()
OR
EXISTS (
SELECT 1 FROM venues v
WHERE v.id = (string_to_array(name, '/'))[1]::uuid
AND public.has_permission(auth.uid(), v.account_id, 'venues.manage')
)
)
);
Use for sensitive documents only admins should access:
CREATE POLICY admin_only_documents ON storage.objects FOR ALL USING (
bucket_id = 'admin-documents'
AND public.is_super_admin()
)
WITH CHECK (
bucket_id = 'admin-documents'
AND public.is_super_admin()
);
NEVER use getPublicUrl() for private buckets:
// ❌ WRONG - Returns 403 for private buckets
const { data } = client.storage.from('private-bucket').getPublicUrl(path);
// ✅ CORRECT - Use signed URLs for private buckets
const { data } = await client.storage
.from(StorageBuckets.INVOICE_PDFS)
.createSignedUrl(path, SignedUrlExpiry.DOWNLOAD);
Use StorageUrlService for consistent URL generation:
import { createStorageUrlService, StorageBuckets, SignedUrlExpiry } from '@kit/shared/storage';
const storageService = createStorageUrlService(client);
// Single URL
const result = await storageService.getSignedUrl(
StorageBuckets.VENUE_DOCUMENTS,
'venue-123/photo.jpg',
{ expiresIn: SignedUrlExpiry.IMMEDIATE_DISPLAY }
);
// Batch URLs (more efficient)
const results = await storageService.getBatchSignedUrls(
StorageBuckets.VENUE_DOCUMENTS,
['path1.jpg', 'path2.jpg', 'path3.jpg'],
{ expiresIn: SignedUrlExpiry.DOWNLOAD }
);
// Enrich array with URLs
const docsWithUrls = await storageService.enrichWithSignedUrls(
StorageBuckets.VENUE_DOCUMENTS,
documents,
(doc) => doc.storage_path,
(doc, url) => ({ ...doc, signedUrl: url }),
{ expiresIn: SignedUrlExpiry.DOWNLOAD }
);
-- List all storage buckets
SELECT id, name, public, created_at
FROM storage.buckets
ORDER BY name;
-- View storage policies
SELECT policyname, cmd, roles, qual, with_check
FROM pg_policies
WHERE schemaname = 'storage' AND tablename = 'objects'
ORDER BY policyname;
-- Check which bucket a policy applies to
SELECT policyname, qual
FROM pg_policies
WHERE schemaname = 'storage'
AND qual LIKE '%bucket_id%';
-- Find files in a bucket
SELECT id, name, bucket_id, created_at
FROM storage.objects
WHERE bucket_id = 'my-bucket'
ORDER BY created_at DESC
LIMIT 10;
/**
* Migration: Update {bucket_name} storage RLS policy
*
* Purpose: Add super admin bypass to {bucket_name} bucket
*/
-- Drop existing policy
DROP POLICY IF EXISTS {bucket_name} ON storage.objects;
-- Create new policy with super admin bypass
CREATE POLICY {bucket_name} ON storage.objects FOR ALL USING (
bucket_id = '{bucket_name}'
AND (
-- Super admin can access all files
public.is_super_admin()
OR
-- Your access conditions here
{your_using_condition}
)
)
WITH CHECK (
bucket_id = '{bucket_name}'
AND (
-- Super admin can manage all files
public.is_super_admin()
OR
-- Your write conditions here
{your_with_check_condition}
)
);
| Error | Cause | Fix | |-------|-------|-----| | "Object not found" | File doesn't exist or RLS blocking | Check path and RLS policy | | "new row violates row-level security policy" | WITH CHECK failing | Add is_super_admin() bypass | | "Bucket not found" | Bucket doesn't exist | Create bucket in storage.buckets | | Signed URL returns 403 | Expired or wrong bucket | Check expiry time and bucket name | | Public URL returns 403 | Bucket is private | Use signed URLs instead |
-- =====================================================================
-- RLS Policies for {table_name}
-- =====================================================================
-- Enable RLS
ALTER TABLE "public".{table_name} ENABLE ROW LEVEL SECURITY;
-- service_role bypass (ALWAYS include this)
DROP POLICY IF EXISTS {table_name}_service_role_bypass ON public.{table_name};
CREATE POLICY {table_name}_service_role_bypass ON public.{table_name}
FOR ALL TO service_role
USING (true) WITH CHECK (true);
-- SELECT
DROP POLICY IF EXISTS {table_name}_select ON public.{table_name};
CREATE POLICY {table_name}_select ON public.{table_name}
FOR SELECT TO authenticated
USING (public.is_super_admin() OR {your_condition});
-- INSERT
DROP POLICY IF EXISTS {table_name}_insert ON public.{table_name};
CREATE POLICY {table_name}_insert ON public.{table_name}
FOR INSERT TO authenticated
WITH CHECK (public.is_super_admin() OR {your_condition});
-- UPDATE
DROP POLICY IF EXISTS {table_name}_update ON public.{table_name};
CREATE POLICY {table_name}_update ON public.{table_name}
FOR UPDATE TO authenticated
USING (public.is_super_admin() OR {your_condition})
WITH CHECK (public.is_super_admin() OR {your_condition});
-- DELETE
DROP POLICY IF EXISTS {table_name}_delete ON public.{table_name};
CREATE POLICY {table_name}_delete ON public.{table_name}
FOR DELETE TO authenticated
USING (public.is_super_admin());
| Metric | Staging | Production | |--------|---------|------------| | Total policies | 311 | 314 | | Missing in prod | 0 | - | | Extra in prod | - | 3 (expected) |
| Table | Staging | Production | Status | |-------|---------|------------|--------| | productions | 9 | 9 | ✅ | | events | 16 | 16 | ✅ | | cast_assignments | 7 | 7 | ✅ | | event_participants | 9 | 9 | ✅ | | invoices | 6 | 6 | ✅ | | hire_orders | 12 | 12 | ✅ | | profiles | 4 | 4 | ✅ | | venues | 8 | 8 | ✅ | | ratings | 7 | 7 | ✅ | | accounts | 6 | 6 | ✅ |
cast_roles.cast_roles_update_authenticated - Client user accessseasons.seasons_select_authenticated - Seasons table (prod only)seasons.seasons_super_admin_all - Seasons table (prod only)-- "Allow all" policies are SECURITY RISKS
DROP POLICY IF EXISTS "Allow all operations for everyone" ON public.{table};
When you see both:
{table}_select AND {table}_select_restricted{table}_delete_own AND {table}_delete_restrictedKeep the more complete one (usually has is_super_admin() bypass).
| Old Name | New Name |
|----------|----------|
| "Users can read own..." | {table}_select |
| "Super admins can manage..." | {table}_super_admin_all |
| {table}_select_restricted | {table}_select |
apps/web/supabase/migrations/docs/08-DATABASE_ARCHITECTURE.mddocs/14-AUTHENTICATION_AUTHORIZATION.mdapps/web/supabase/schemas/accounts_memberships recordsaccounts_memberships usagetools
# Test Patterns Testing patterns for reliable, maintainable, and fast tests. > **Template Usage:** Customize for your test framework (Vitest, Jest, Playwright, etc.) and assertion library. ## Test Structure ```typescript // user.test.ts import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { userService } from '@/services/user.service'; import { createTestUser, cleanupTestData } from '@/tests/helpers'; describe('UserService', () => { let testUserId: string; befor
tools
# State Management Patterns Client-side state management patterns for modern applications. > **Template Usage:** Customize for your state library (React Query, Zustand, Jotai, Redux, etc.). ## State Categories | Type | Description | Solution | |------|-------------|----------| | **Server State** | Data from API/database | React Query, SWR | | **Client State** | UI state, user preferences | Zustand, Jotai, useState | | **Form State** | Form inputs, validation | React Hook Form, Formik | | **U
development
# Service Patterns Service layer patterns for clean architecture with proper error handling, logging, and type safety. > **Template Usage:** Customize for your ORM (Prisma, Drizzle, TypeORM, etc.) and logging solution. ## Result Type Pattern Never throw exceptions from services. Always return a Result type. ```typescript // lib/result.ts export type Result<T, E = Error> = | { success: true; data: T } | { success: false; error: E }; export function ok<T>(data: T): Result<T, never> { r
testing
# Row-Level Security Patterns Database security patterns for multi-tenant and user-scoped data. > **Template Usage:** Customize for your database (PostgreSQL, Supabase, etc.) and auth system. ## RLS Fundamentals ### Enable RLS on Tables ```sql -- Enable RLS (required before policies take effect) ALTER TABLE users ENABLE ROW LEVEL SECURITY; ALTER TABLE posts ENABLE ROW LEVEL SECURITY; ALTER TABLE comments ENABLE ROW LEVEL SECURITY; -- Force RLS for table owners too (recommended) ALTER TABLE