| name | supabase-migration |
| description | Safe database migration creation and management for Supabase PostgreSQL |
| version | 1.0.0 |
Supabase Migration Skill
Purpose
Create, test, and apply database migrations safely following project conventions.
Migration Conventions
File Naming Pattern
YYYYMMDDHHMMSS_description.sql
Examples:
20251018120000_add_user_preferences.sql
20251018130000_enable_rls_on_comments.sql
Location
/home/sk/template-copilot-kit-py/supabase/migrations/
Core Principles
1. Idempotency (CRITICAL)
Always use:
CREATE TABLE IF NOT EXISTS
ALTER TABLE ... ADD COLUMN IF NOT EXISTS (PostgreSQL 9.6+)
DROP TABLE IF EXISTS
DO $$ ... END $$; blocks for conditional logic
Never use:
CREATE TABLE without IF NOT EXISTS
ALTER TABLE ADD COLUMN without IF NOT EXISTS
- Any command that fails on re-run
2. RLS Security (REQUIRED)
Every new table MUST include:
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can manage own records"
ON table_name
FOR ALL
TO authenticated
USING (profile_id = auth.uid())
WITH CHECK (profile_id = auth.uid());
3. Foreign Keys
Use profile_id, NOT user_id:
profile_id UUID REFERENCES profiles(id) ON DELETE CASCADE
user_id UUID REFERENCES auth.users(id)
Migration Template
BEGIN;
CREATE TABLE IF NOT EXISTS table_name (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
name TEXT NOT NULL,
status TEXT DEFAULT 'active'
);
CREATE INDEX IF NOT EXISTS idx_table_profile
ON table_name(profile_id);
CREATE INDEX IF NOT EXISTS idx_table_created
ON table_name(created_at DESC);
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "policy_name" ON table_name;
CREATE POLICY "Users manage own records"
ON table_name
FOR ALL
TO authenticated
USING (profile_id = auth.uid())
WITH CHECK (profile_id = auth.uid());
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_update_updated_at ON table_name;
CREATE TRIGGER trigger_update_updated_at
BEFORE UPDATE ON table_name
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
COMMIT;
Workflow
1. Create Migration File
cd /home/sk/template-copilot-kit-py/supabase/migrations
touch $(date +%Y%m%d%H%M%S)_description.sql
nano <filename>.sql
2. Test Locally
supabase db push
3. Verify Migration
SELECT tablename FROM pg_tables
WHERE tablename = 'your_table_name';
SELECT tablename, rowsecurity
FROM pg_tables
WHERE tablename = 'your_table_name';
SELECT schemaname, tablename, policyname
FROM pg_policies
WHERE tablename = 'your_table_name';
INSERT INTO your_table_name (profile_id, name)
VALUES (auth.uid(), 'Test record')
RETURNING *;
4. Rollback (if needed)
touch $(date +%Y%m%d%H%M%S)_rollback_previous_migration.sql
Rollback template:
BEGIN;
DROP TABLE IF EXISTS table_name CASCADE;
COMMIT;
Common Patterns
Adding Column to Existing Table
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'presentations'
AND column_name = 'new_column'
) THEN
ALTER TABLE presentations
ADD COLUMN new_column TEXT;
END IF;
END $$;
Creating RPC Function
CREATE OR REPLACE FUNCTION function_name(param1 TEXT)
RETURNS TABLE (id UUID, name TEXT) AS $$
BEGIN
RETURN QUERY
SELECT t.id, t.name
FROM table_name t
WHERE t.profile_id = auth.uid()
AND t.status = param1;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
Adding Public Access Policy
CREATE POLICY "Public presentations visible to all"
ON presentations
FOR SELECT
TO anon, authenticated
USING (is_public = true);
Testing Checklist
Pre-Apply
Post-Apply
Debugging
Check Migration Status
supabase migration list
supabase db diff
Common Errors
Error: "relation already exists"
- Cause: Missing
IF NOT EXISTS
- Fix: Add
IF NOT EXISTS to CREATE statements
Error: "column already exists"
- Cause: Missing conditional column check
- Fix: Use DO block with
information_schema check
Error: "violates foreign key constraint"
- Cause: Referenced table doesn't exist
- Fix: Ensure migration order is correct
Error: "permission denied for table"
- Cause: RLS blocking query
- Fix: Add appropriate RLS policy
Security Rules
Always Include
- RLS enabled on all tables with user data
- profile_id check in policies:
profile_id = auth.uid()
- CASCADE delete on foreign keys
- SECURITY DEFINER on RPC functions (when needed)
Never Include
- user_id foreign keys (use
profile_id)
- auth.users direct references
- Hardcoded secrets or API keys
- Production user data in migrations
Quick Reference
Create Migration
cd /home/sk/template-copilot-kit-py/supabase/migrations
touch $(date +%Y%m%d%H%M%S)_description.sql
Apply Migration
supabase db push
Check RLS Status
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
Verify Policies
SELECT tablename, policyname, cmd
FROM pg_policies
ORDER BY tablename;
Example: Complete Migration
File: 20251018120000_add_comments_table.sql
BEGIN;
CREATE TABLE IF NOT EXISTS comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
presentation_id UUID NOT NULL REFERENCES presentations(id) ON DELETE CASCADE,
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_comments_presentation
ON comments(presentation_id);
CREATE INDEX IF NOT EXISTS idx_comments_profile
ON comments(profile_id);
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "Users manage own comments" ON comments;
CREATE POLICY "Users manage own comments"
ON comments FOR ALL TO authenticated
USING (profile_id = auth.uid())
WITH CHECK (profile_id = auth.uid());
DROP POLICY IF EXISTS "Public comments visible" ON comments;
CREATE POLICY "Public comments visible"
ON comments FOR SELECT TO anon, authenticated
USING (
EXISTS (
SELECT 1 FROM presentations
WHERE id = comments.presentation_id
AND is_public = true
)
);
COMMIT;
This skill ensures all migrations are safe, idempotent, and follow project security standards.