| name | Database Schema Extension |
| description | 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. |
Database Schema Extension
This skill guides you through extending the database schema using declarative schema files, Supabase migrations, and automatic type generation.
Quick Reference
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
Complete Workflow
Step 1: Modify Declarative Schema Files
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.
Step 2: Follow SQL Style Guidelines
Naming Conventions:
- Tables:
snake_case, plural (e.g.,todos,user_profiles) - Columns:
snake_case, singular (e.g.,user_id,created_at) - Enums:
snake_case, singular (e.g.,priority_level,user_role). Always prefer enums over text for fixed sets. - Foreign keys:
{singular_table_name}_id(e.g.,user_idreferencesusers) - Indexes:
{table}_{column}_idx(e.g.,todos_user_id_idx) - Policies: Descriptive text in quotes (e.g.,
"Users can view their own todos"). Keep them short and clear.
SQL Standards:
- All SQL keywords in lowercase (e.g.,
create table,select,where) - Always use
publicschema prefix (e.g.,public.todos) - Add table comments:
comment on table public.todos is 'User todo items' - Add column comments for enums:
comment on column public.todos.priority is 'Priority level: low, medium, or high' - Always prefer enums over text for fixed sets.
- Use
timestamptzfor timestamps (includes timezone) - Default timestamps:
created_at timestamptz default now() not null - Use
uuidfor primary keys:id uuid default gen_random_uuid() primary key
Example 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';
Step 3: Add Row Level Security (RLS)
Critical RLS Rules:
- Always enable RLS on new tables (even public tables)
- Create separate policies for each operation (select, insert, update, delete)
- Specify roles explicitly using
to authenticatedorto anon - Add indexes on columns used in policies (usually
user_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:
- SELECT policies: Use
usingonly (notwith check) - INSERT policies: Use
with checkonly (notusing) - UPDATE policies: Use both
usingandwith check - DELETE policies: Use
usingonly (notwith check) - Never use
FOR ALL: Always separate into individual policies - Avoid joins: Rewrite policies to use
INorANYinstead
Public 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');
Step 4: Add SQL Functions and Triggers
SQL Function Best Practices:
- Default to
security invoker(run with caller's permissions) - Set
search_path = ''and use fully qualified names - Use explicit typing for parameters and return values
- Declare as
immutableorstablewhen possible for optimization
Common 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 = '';
Step 5: Generate Migration
After modifying schema files, generate a migration:
bun db:diff add_bookings_table
What This Does:
- Compares
supabase/schemas/*.sqlwith your local database - Generates SQL migration in
supabase/migrations/YYYYMMDDHHMMSS_add_bookings_table.sql - Shows you the diff for review
Important: Always review the generated migration SQL before applying!
Step 6: Review and Apply Migration
Review the migration file in
supabase/migrations/Check for:
- Destructive operations (drop, truncate, alter column types)
- Missing RLS policies
- Correct foreign key relationships
- Proper indexes
Apply the migration:
bun migrate:up
Step 7: Regenerate Types
Critical Final Step: Always regenerate types after schema changes!
bun gen:types
What This Does:
- Runs
bun db:types- Generates TypeScript types from database - Runs
bun db:types:zod- Generates Zod schemas from TypeScript types - Runs
bun remove:public:prefix- Cleans up schema names
Generated Files:
types/database.types.ts- TypeScript types for all tables, enums, functionsschemas/database.schema.ts- Zod schemas for validation
Usage 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);
Troubleshooting
Migration Conflicts
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 dataCheck if you have unapplied migrations:
bun migrate:up
Type Generation Fails
If bun gen:types fails:
Ensure local database is running:
bun db:startCheck for SQL syntax errors in schema files
Verify all migrations are applied:
bun migrate:up
Policy Not Working
Common issues:
- RLS not enabled:
alter table public.my_table enable row level security; - Missing role specification: Add
to authenticatedorto anon - Missing index: Add index on
user_idor columns used in policy - Function not wrapped in select: Use
(select auth.uid())notauth.uid()
Workflow Checklist
When extending the database schema, follow this checklist:
- Modify appropriate schema file (
00-extensions.sql,01-schema.sql,02-policies.sql, or03-functions.sql) - Follow SQL style guide (lowercase, snake_case, schema prefix)
- Add table and column comments
- Create indexes for foreign keys and frequently queried columns
- Enable RLS on new tables
- Create separate policies for select/insert/update/delete
- Add
updated_attrigger if table hasupdated_atcolumn - Run
bun db:diff <migration_name>to generate migration - Review generated migration SQL
- Run
bun migrate:upto apply migration - Run
bun gen:typesto regenerate TypeScript/Zod types - Test new schema in application code
Best Practices Summary
- Always work declaratively - Edit schema files, let Supabase generate migrations
- One migration per logical change - Don't bundle unrelated changes
- Review before applying - Always check generated SQL
- Regenerate types immediately - Run
bun gen:typesafter every schema change - Enable RLS by default - Security first, even for "public" tables
- Index foreign keys - Always add indexes on reference columns
- Use timestamps - Add
created_atandupdated_atto most tables - Comment everything - Future you will thank present you
- Test locally first - Use local database, never modify production directly
- Follow naming conventions - Consistency makes collaboration easier
References
- Project package.json scripts:
/package.json - Existing schema examples:
/supabase/schemas/01-schema.sql - RLS policy examples:
/supabase/schemas/02-policies.sql - Function examples:
/supabase/schemas/03-functions.sql