Extend the Supabase PostgreSQL database schema following this project's declarative schema patterns, migration workflow, and type generation pipeline. Use when adding tables, columns, enums, RLS policies, triggers, or database functions.
This skill guides you through extending the database schema using declarative schema files, Supabase migrations, and automatic type generation.
Key Commands:
bun db:diff <migration_name> - Generate migration from schema changesbun migrate:up - Apply migrations to local databasebun gen:types - Regenerate TypeScript types and Zod schemasbun db:reset - Reset database (destructive!)Schema File Organization:
supabase/schemas/
├── 00-extensions.sql # PostgreSQL extensions
├── 01-schema.sql # Tables, enums, indexes
├── 02-policies.sql # Row Level Security policies
└── 03-functions.sql # Database functions and triggers
The project uses declarative schema files in supabase/schemas/. These files define the desired state of your database, and Supabase CLI generates migrations by comparing them with your local database.
Naming Conventions:
snake_case, plural (e.g., todos, user_profiles)snake_case, singular (e.g., user_id, created_at)snake_case, singular (e.g., priority_level, user_role). Always prefer enums over text for fixed sets.{singular_table_name}_id (e.g., user_id references users){table}_{column}_idx (e.g., todos_user_id_idx)"Users can view their own todos"). Keep them short and clear.SQL Standards:
create table, select, where)public schema prefix (e.g., public.todos)comment on table public.todos is 'User todo items'comment on column public.todos.priority is 'Priority level: low, medium, or high'timestamptz for timestamps (includes timezone)created_at timestamptz default now() not nulluuid for primary keys: id uuid default gen_random_uuid() primary keyExample Table Creation:
-- Priority enum type
create type public.priority_level as enum ('low', 'medium', 'high');
-- Todos table
create table public.todos (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
title text not null,
description text,
completed boolean default false not null,
priority public.priority_level,
due_date timestamptz,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null
);
-- Indexes for performance
create index todos_user_id_idx on public.todos(user_id);
create index todos_completed_idx on public.todos(completed);
create index todos_due_date_idx on public.todos(due_date);
-- Comments for documentation
comment on table public.todos is 'User todo items';
comment on column public.todos.priority is 'Priority level: low, medium, or high';
Critical RLS Rules:
to authenticated or to anonuser_id)Policy Structure:
-- Enable RLS
alter table public.todos enable row level security;
-- SELECT policy
create policy "Users can view their own todos"
on public.todos for select
to authenticated
using (auth.uid() = user_id);
-- INSERT policy
create policy "Users can create their own todos"
on public.todos for insert
to authenticated
with check (auth.uid() = user_id);
-- UPDATE policy
create policy "Users can update their own todos"
on public.todos for update
to authenticated
using (auth.uid() = user_id)
with check (auth.uid() = user_id);
-- DELETE policy
create policy "Users can delete their own todos"
on public.todos for delete
to authenticated
using (auth.uid() = user_id);
Key Policy Guidelines:
using only (not with check)with check only (not using)using and with checkusing only (not with check)FOR ALL: Always separate into individual policiesIN or ANY insteadPublic Access Example:
-- Public read access
create policy "Avatar media is viewable by everyone"
on public.media for select
to authenticated, anon
using (media_type = 'avatar');
SQL Function Best Practices:
security invoker (run with caller's permissions)search_path = '' and use fully qualified namesimmutable or stable when possible for optimizationCommon Pattern: updated_at Trigger
-- Reuse existing function for updated_at
create trigger my_table_updated_at
before update on public.my_table
for each row
execute function public.handle_updated_at();
The project already has public.handle_updated_at() function - just create the trigger!
Custom Function Example:
-- RPC function example
create or replace function public.get_user_stats(user_uuid uuid)
returns table (
user_id uuid,
total_items bigint,
completed_items bigint
) as $$
begin
return query
select
user_uuid as user_id,
count(*) as total_items,
count(*) filter (where completed = true) as completed_items
from public.todos
where user_id = user_uuid;
end;
$$ language plpgsql security invoker set search_path = '';
After modifying schema files, generate a migration:
bun db:diff add_bookings_table
What This Does:
supabase/schemas/*.sql with your local databasesupabase/migrations/YYYYMMDDHHMMSS_add_bookings_table.sqlImportant: Always review the generated migration SQL before applying!
Review the migration file in supabase/migrations/
Check for:
Apply the migration:
bun migrate:up
Critical Final Step: Always regenerate types after schema changes!
bun gen:types
What This Does:
bun db:types - Generates TypeScript types from databasebun db:types:zod - Generates Zod schemas from TypeScript typesbun remove:public:prefix - Cleans up schema namesGenerated Files:
types/database.types.ts - TypeScript types for all tables, enums, functionsschemas/database.schema.ts - Zod schemas for validationUsage in Code:
// Import types
import type { Database } from "@/types/database.types";
// Use table types
type Booking = Database["public"]["Tables"]["bookings"]["Row"];
type BookingInsert = Database["public"]["Tables"]["bookings"]["Insert"];
type BookingUpdate = Database["public"]["Tables"]["bookings"]["Update"];
// Import Zod schemas
import { bookingsInsertSchema } from "@/schemas/database.schema";
// Use in server actions
const { data: validatedData, success } = bookingsInsertSchema.safeParse(input);
If bun db:diff shows unexpected changes:
Check if local database is out of sync:
bun db:reset # Resets local DB to match migrations + seed data
Check if you have unapplied migrations:
bun migrate:up
If bun gen:types fails:
Ensure local database is running:
bun db:start
Check for SQL syntax errors in schema files
Verify all migrations are applied:
bun migrate:up
Common issues:
alter table public.my_table enable row level security;to authenticated or to anonuser_id or columns used in policy(select auth.uid()) not auth.uid()When extending the database schema, follow this checklist:
00-extensions.sql, 01-schema.sql, 02-policies.sql, or 03-functions.sql)updated_at trigger if table has updated_at columnbun db:diff <migration_name> to generate migrationbun migrate:up to apply migrationbun gen:types to regenerate TypeScript/Zod typesbun gen:types after every schema changecreated_at and updated_at to most tables/package.json/supabase/schemas/01-schema.sql/supabase/schemas/02-policies.sql/supabase/schemas/03-functions.sql