Database Schema Extension
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.
$ 安裝
git clone https://github.com/Reodor-Studios/create-reodor-app /tmp/create-reodor-app && cp -r /tmp/create-reodor-app/.claude/skills/database-schema-extension ~/.claude/skills/create-reodor-app// tip: Run this command in your terminal to install the skill
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 data -
Check if you have unapplied migrations:
bun migrate:up
Type Generation Fails
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
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
Repository
