From saas-toolkit
Supabase database migrations. Use when creating tables, modifying schema, adding RLS policies, or writing seed data.
How this skill is triggered — by the user, by Claude, or both
Slash command
/saas-toolkit:db-migrationThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create and manage Supabase database migrations with proper patterns for SaaS applications.
Create and manage Supabase database migrations with proper patterns for SaaS applications.
All migrations target Supabase Cloud via MCP. Use mcp__supabase to run SQL directly on the cloud project. Never use supabase start or supabase db push.
Write idempotent SQL that can be re-run safely. For local tracking, keep files in supabase/migrations/ but apply them via MCP.
Always write migrations that can be re-run safely:
-- Create table (idempotent)
CREATE TABLE IF NOT EXISTS public.my_table (...);
-- Add column (idempotent)
ALTER TABLE public.my_table ADD COLUMN IF NOT EXISTS new_column text;
-- Create index (idempotent)
CREATE INDEX IF NOT EXISTS idx_my_table_column ON public.my_table(column);
-- Enable RLS (idempotent)
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
-- Create policy (drop + create for idempotency)
DROP POLICY IF EXISTS "policy_name" ON public.my_table;
CREATE POLICY "policy_name" ON public.my_table ...;
Run the SQL directly on the cloud project using mcp__supabase SQL execution tools. Do NOT use supabase db push or local development.
npx supabase gen types typescript --project-id <project-id> > lib/database.types.ts
Every new table must have:
id uuid DEFAULT gen_random_uuid() PRIMARY KEYcreated_at timestamptz DEFAULT now() NOT NULLupdated_at timestamptz DEFAULT now() NOT NULLALTER TABLE public.xxx ENABLE ROW LEVEL SECURITY;updated_at trigger-- Create the trigger function (once per project)
CREATE OR REPLACE FUNCTION public.handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Apply to table
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON public.my_table
FOR EACH ROW
EXECUTE FUNCTION public.handle_updated_at();
CREATE POLICY "Users can view own data" ON public.my_table
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can create own data" ON public.my_table
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own data" ON public.my_table
FOR UPDATE USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can delete own data" ON public.my_table
FOR DELETE USING (auth.uid() = user_id);
CREATE POLICY "Org members can view data" ON public.my_table
FOR SELECT USING (
EXISTS (
SELECT 1 FROM public.org_members
WHERE org_members.org_id = my_table.org_id
AND org_members.user_id = auth.uid()
)
);
CREATE POLICY "Only admins can modify" ON public.my_table
FOR ALL USING (
EXISTS (
SELECT 1 FROM public.org_members
WHERE org_members.org_id = my_table.org_id
AND org_members.user_id = auth.uid()
AND org_members.role = 'admin'
)
);
CREATE POLICY "Anyone can read" ON public.my_table
FOR SELECT USING (true);
CREATE TABLE public.profiles (
id uuid REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
full_name text,
avatar_url text,
stripe_customer_id text UNIQUE,
created_at timestamptz DEFAULT now() NOT NULL,
updated_at timestamptz DEFAULT now() NOT NULL
);
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view own profile"
ON public.profiles FOR SELECT USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON public.profiles FOR UPDATE USING (auth.uid() = id);
-- Auto-create profile on signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, full_name, avatar_url)
VALUES (NEW.id, NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'avatar_url');
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();
CREATE TABLE public.organizations (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
name text NOT NULL,
slug text UNIQUE NOT NULL,
stripe_customer_id text UNIQUE,
created_at timestamptz DEFAULT now() NOT NULL,
updated_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE public.org_members (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
org_id uuid REFERENCES public.organizations(id) ON DELETE CASCADE NOT NULL,
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
role text NOT NULL DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
created_at timestamptz DEFAULT now() NOT NULL,
UNIQUE(org_id, user_id)
);
CREATE INDEX idx_org_members_user_id ON public.org_members(user_id);
CREATE INDEX idx_org_members_org_id ON public.org_members(org_id);
Note: If using @supabase/stripe-sync-engine (recommended), the stripe.* schema manages subscription data automatically. Only create this manual table if you have a specific reason not to use the sync engine.
CREATE TABLE public.subscriptions (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
stripe_customer_id text NOT NULL,
stripe_subscription_id text UNIQUE,
stripe_price_id text,
status text NOT NULL DEFAULT 'inactive',
current_period_start timestamptz,
current_period_end timestamptz,
cancel_at_period_end boolean DEFAULT false,
created_at timestamptz DEFAULT now() NOT NULL,
updated_at timestamptz DEFAULT now() NOT NULL
);
CREATE INDEX idx_subscriptions_user_id ON public.subscriptions(user_id);
CREATE INDEX idx_subscriptions_stripe_customer_id ON public.subscriptions(stripe_customer_id);
CREATE TABLE public.audit_logs (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
user_id uuid REFERENCES auth.users(id) ON DELETE SET NULL,
org_id uuid REFERENCES public.organizations(id) ON DELETE SET NULL,
action text NOT NULL,
entity_type text NOT NULL,
entity_id uuid,
metadata jsonb DEFAULT '{}',
ip_address inet,
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE INDEX idx_audit_logs_user_id ON public.audit_logs(user_id);
CREATE INDEX idx_audit_logs_org_id ON public.audit_logs(org_id);
CREATE INDEX idx_audit_logs_created_at ON public.audit_logs(created_at);
For local development, add seed data in supabase/seed.sql:
-- Only runs on local reset, not on production
INSERT INTO public.organizations (id, name, slug) VALUES
('00000000-0000-0000-0000-000000000001', 'Acme Corp', 'acme');
created_at and updated_attimestamptz not timestamptext not varcharuuid primary keysmcp__supabase — never localstripe.* schema is managed by stripe-sync-engine — only add RLS policies to stripe.* tables, never create/modify the tables themselvesnpx claudepluginhub mickaelmamani/saas-toolkit --plugin saas-toolkitGenerates Supabase Postgres schemas from requirements with migrations, RLS policies, indexes, triggers, and TypeScript types for production apps.
Guides SaaS database selection (Supabase/PostgreSQL preferred), multi-tenant schema design, Row Level Security setup, migrations, queries, and data troubleshooting.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.