Why Supabase Needs 'Use the Platform' Rules
Supabase is a platform, not just a database — it provides authentication, row-level security, file storage, realtime subscriptions, and edge functions out of the box. AI assistants treat Supabase as a plain PostgreSQL database: building custom auth with JWT, custom file upload with S3, and custom realtime with WebSockets. Every custom implementation is more code, more bugs, and more maintenance than using Supabase's built-in features.
The most common AI failures: custom auth (bcrypt + JWT) instead of Supabase Auth, no RLS policies (every user can read every row), manual file handling instead of Storage buckets, polling for updates instead of Realtime channels, custom API routes instead of Edge Functions, and using the raw PostgreSQL connection instead of supabase-js client.
These rules enforce a simple principle: if Supabase provides it, use it. Don't build what the platform already handles — that's why you chose Supabase.
Rule 1: Supabase Auth for Everything
The rule: 'Use Supabase Auth for all authentication: supabase.auth.signInWithPassword({ email, password }), supabase.auth.signInWithOAuth({ provider: "google" }), supabase.auth.signUp({ email, password }). Never build custom auth — no bcrypt, no JWT generation, no session table. Supabase Auth handles: email/password, OAuth (Google, GitHub, etc.), magic links, phone auth, and MFA.'
For session management: 'Supabase Auth manages sessions automatically. Use supabase.auth.getSession() for the current session. Use supabase.auth.onAuthStateChange() for reactive auth state. The supabase-js client automatically includes the auth token in all database and storage requests — no manual header management.'
For server-side auth: 'In server environments (Next.js API routes, Edge Functions), use createServerClient or createClient with the service role key for admin operations. Never expose the service role key to the client — it bypasses RLS. Use the anon key for client-side operations — it respects RLS policies.'
- supabase.auth.signInWithPassword/signUp/signInWithOAuth — never custom auth
- supabase.auth.getSession() — supabase.auth.onAuthStateChange() for reactive
- Client auto-includes auth token in DB/storage requests — no manual headers
- anon key for client-side (respects RLS) — service role key for server admin only
- Never expose service role key to the client — it bypasses all security
Supabase Auth handles email, OAuth, magic links, phone, and MFA. The client auto-includes tokens in DB/storage requests. Building custom auth with bcrypt + JWT on Supabase is reimplementing what's already free and battle-tested.
Rule 2: Row Level Security (RLS) on Every Table
The rule: 'Enable RLS on every table: ALTER TABLE posts ENABLE ROW LEVEL SECURITY. Define policies for each operation: CREATE POLICY "Users read own posts" ON posts FOR SELECT USING (auth.uid() = user_id). Without RLS, the anon key gives every user access to every row in every table. RLS is not optional — it's Supabase's authorization layer.'
For common policies: 'Read own data: USING (auth.uid() = user_id). Insert own data: WITH CHECK (auth.uid() = user_id). Public read: USING (true) — for blog posts, product catalogs. Admin access: USING (auth.jwt() ->> 'role' = 'admin'). Never use USING (true) on tables with sensitive data — it makes everything public.'
AI generates tables without RLS — the database is wide open. One missing policy means every authenticated user (or worse, every anonymous request) can read, modify, or delete any row. RLS policies are your first and most important security layer in Supabase.
Without RLS, the anon key gives every user access to every row in every table. One missing policy = entire database exposed. Enable RLS on every table, define policies for every operation. No exceptions.
Rule 3: Storage Buckets for Files
The rule: 'Use Supabase Storage for all file operations: supabase.storage.from("avatars").upload(path, file). Never upload files to your own server or directly to S3. Storage buckets integrate with: RLS (file-level access control), CDN (automatic edge caching), transformations (resize images on-the-fly), and auth (signed URLs for private files).'
For bucket configuration: 'Create buckets in the Supabase dashboard or with SQL. Set public: true for publicly accessible files (avatars, product images). Set public: false for private files (documents, reports) — access via signed URLs. Define storage policies like RLS: storage.objects policies control who can upload, download, and delete.'
For file handling: 'Upload: supabase.storage.from("bucket").upload("folder/file.png", file). Download: supabase.storage.from("bucket").download("folder/file.png"). Public URL: supabase.storage.from("bucket").getPublicUrl("folder/file.png"). Signed URL (private, expiring): supabase.storage.from("bucket").createSignedUrl("path", 3600).'
- Storage buckets for all files — never custom S3 or server uploads
- Public buckets: avatars, product images — Private: documents, reports
- Storage policies for access control — like RLS but for files
- getPublicUrl for public — createSignedUrl for private with expiration
- Image transformations on-the-fly — resize, crop without pre-processing
Rule 4: Realtime for Live Updates
The rule: 'Use Supabase Realtime for live data updates — never poll with setInterval. Subscribe to database changes: supabase.channel("orders").on("postgres_changes", { event: "INSERT", schema: "public", table: "orders" }, (payload) => { addOrder(payload.new) }).subscribe(). Realtime supports: INSERT, UPDATE, DELETE events, filtered by column values, with RLS enforcement.'
For channels: 'Use Realtime Channels for broadcast (pub/sub) and presence (who's online): supabase.channel("room-1").on("broadcast", { event: "cursor" }, handleCursor).subscribe(). Presence tracks connected users: channel.on("presence", { event: "sync" }, () => { const state = channel.presenceState() }). Use Realtime for: live dashboards, collaborative editing, chat, notifications.'
AI generates polling (setInterval + fetch every 5 seconds) instead of Realtime subscriptions. Polling wastes bandwidth, adds latency, and doesn't scale. Realtime is push-based: the server notifies when data changes, instantly, with zero client-side polling.
setInterval + fetch every 5 seconds wastes bandwidth and adds latency. Supabase Realtime pushes changes instantly over WebSocket. One subscription replaces polling — less code, less bandwidth, real-time updates.
Rule 5: Typed Client and Edge Functions
The rule: 'Generate TypeScript types from your database schema: supabase gen types typescript --project-id your-project > database.types.ts. Create a typed client: createClient<Database>(url, key). All queries are type-safe: supabase.from("users").select("id, name, email") returns typed results. Regenerate types after every migration.'
For Edge Functions: 'Use Supabase Edge Functions (Deno runtime) for server-side logic: supabase functions new my-function. Functions run on the edge — close to users, fast cold start. Use for: webhooks, scheduled tasks, third-party API integration, and server-side validation. Call from client: supabase.functions.invoke("my-function", { body: data }).'
For the database client: 'Use supabase-js for all database operations — never raw SQL from the client. The client handles: auth token injection, RLS enforcement, type safety, and connection management. Use .select() for reads, .insert() for creates, .update() for modifications, .delete() for removals. Use .rpc() for calling PostgreSQL functions.'
- supabase gen types for TypeScript types — regenerate after migrations
- createClient<Database> for typed client — every query is type-safe
- Edge Functions for server logic: webhooks, scheduled tasks, third-party APIs
- supabase-js for all DB ops — never raw SQL from client
- .select(), .insert(), .update(), .delete(), .rpc() — all type-safe
Complete Supabase Rules Template
Consolidated rules for Supabase projects.
- Supabase Auth: signIn/signUp/OAuth — never custom auth, JWT, bcrypt
- RLS on every table — policies for each operation — never USING (true) on sensitive data
- Storage buckets for files — public/private — storage policies for access control
- Realtime subscriptions — never polling — postgres_changes, broadcast, presence
- Typed client: gen types → createClient<Database> — type-safe queries
- Edge Functions for server logic — Deno runtime — invoke from client
- anon key for client — service role key for server admin only — never exposed
- supabase-js for all DB ops — .select, .insert, .update, .delete, .rpc