This skill should be used when the user asks to "create a new migration", "add a database table", "write a migration file", "change the schema", or "add a column to a table". It covers Supabase migration naming conventions, idempotent patterns, timestamptz usage, and safe schema evolution.
Migrations live in supabase/migrations/ and follow the Supabase CLI naming
convention. They are applied in order by timestamp. Each migration should be
idempotent where possible, always use timestamptz, and avoid destructive
changes to existing data.
YYYYMMDDHHMMSS_<verb>_<subject>.sqlUse a descriptive verb and subject. The timestamp ensures ordering.
Examples from this codebase:
20260318100100_extensions.sql
20260318100200_teams.sql
20260318100300_bracket_predictions.sql
20260318100400_expert_picks.sql
20260318100500_user_brackets.sql
20260318100600_bracket_ratings.sql
20260318100700_articles_and_embeddings.sql
20260318100800_chat_history.sql
WRONG:
001_create_tables.sql -- no timestamp
20260318_stuff.sql -- vague name, incomplete timestamp
timestamptz — never timestampAll timestamp columns must use timestamptz (timestamp with time zone).
CORRECT:
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
WRONG:
created_at timestamp NOT NULL DEFAULT now() -- loses timezone info
gen_random_uuid() for primary keysTables with UUID primary keys should use PostgreSQL-generated UUIDs.
Note: some tables (e.g., teams) use composite natural keys instead —
that is fine when the domain calls for it.
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
WRONG:
id serial PRIMARY KEY, -- sequential, predictable
id uuid PRIMARY KEY, -- no default — app must generate
IF NOT EXISTS / IF EXISTS for idempotencyExtensions, functions, and indexes should be idempotent so migrations can be safely re-run during development.
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE INDEX IF NOT EXISTS idx_user_brackets_user_id ON user_brackets(user_id);
DROP INDEX IF EXISTS idx_old_name;
-- Description: User brackets for saving tournament picks.
For schema changes:
DROP TABLE unless it's truly emptyWRONG:
ALTER TABLE teams DROP COLUMN conference; -- data loss
CORRECT:
-- Migration 1: add new column
ALTER TABLE teams ADD COLUMN conf_abbrev text;
-- Migration 2: backfill
UPDATE teams SET conf_abbrev = conference WHERE conference IS NOT NULL;
-- Migration 3: drop old column (after verifying backfill)
ALTER TABLE teams DROP COLUMN conference;
Don't create a table in one migration and add RLS in another — the table is exposed between migrations.
CREATE TABLE my_table ( ... );
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "..." ON my_table ...;