Use when writing DB migrations, creating new tables, generating types, or verifying RLS policies. Load for Phase 3 and any DB-related work.
(SELECT auth.uid()) not just auth.uid() — PostgreSQL caches the subquerypnpm db:gen to regenerate packages/database/src/types.tssupabase start), then push to cloud staging (supabase db push)user_id is always added server-side from auth.getUser() — RLS is the last line of defenseuser_id.-- 002_rls_policies.sql
ALTER TABLE {table} ENABLE ROW LEVEL SECURITY;
CREATE POLICY "{table}_select_own" ON {table}
FOR SELECT USING (user_id = (SELECT auth.uid()));
CREATE POLICY "{table}_insert_own" ON {table}
FOR INSERT WITH CHECK (user_id = (SELECT auth.uid()));
CREATE POLICY "{table}_update_own" ON {table}
FOR UPDATE
USING (user_id = (SELECT auth.uid()))
WITH CHECK (user_id = (SELECT auth.uid()));
CREATE POLICY "{table}_delete_own" ON {table}
FOR DELETE USING (user_id = (SELECT auth.uid()));
supabase/migrations/
001_initial_schema.sql ← all tables
002_rls_policies.sql ← RLS for every table
003_indexes.sql ← performance indexes
004_add_tags_to_chats.sql ← new feature (never edit 001-003)
-- Must return TRUE for every row
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
# After every migration
pnpm db:gen
# = supabase gen types typescript --local > packages/database/src/types.ts
# Local development
supabase start # start Docker containers
supabase db reset # reset + re-run all migrations (DESTRUCTIVE)
pnpm db:gen # regenerate types
# Cloud staging
supabase db push # apply pending migrations to cloud
supabase db push --dry-run # preview what would run
# Never run db:reset on production
// Even with RLS, always filter explicitly in queries
const { data, error } = await supabase
.from('chats')
.select('*')
.eq('user_id', user.id) // ← explicit, even though RLS enforces it
.order('created_at', { ascending: false })
// Server (API routes, Server Components) — uses cookies
import { createServerClient } from '@/lib/supabase/server'
const supabase = await createServerClient()
// Browser (Client Components) — singleton
import { createBrowserClient } from '@/lib/supabase/browser'
const supabase = createBrowserClient()
// Service role (admin operations only, server-side only)
import { createClient } from '@supabase/supabase-js'
const admin = createClient(url, process.env['SUPABASE_SERVICE_ROLE_KEY']!)
// Never expose service role key to client
❌ CREATE TABLE chats (...); -- without RLS policies
❌ auth.uid() = user_id -- use (SELECT auth.uid()) for performance
❌ Editing existing migration files
❌ supabase.auth.getSession() // can return stale data, use getUser()
❌ NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY // exposes secret
❌ supabase.from('chats').select('*') // without .eq('user_id', user.id)
SELECT tablename, rowsecurity FROM pg_tables — all TRUE?pnpm db:gen run after migration?packages/database/src/types.ts committed?