# Supabase Database Schema Management This file contains guidance for working with database schemas, migrations, and Supabase development workflows. ## Schema Organization Schemas are organized in numbered files in the `schemas/` directory. Numbers are used to sort dependencies. Migrations are generated from schemas. If creating a new schema, the migration can be created using the exact same content. If modifying an existing migration, use the `diff` command: ### 1. Creating new entities When creating new entities (such as creating a new tabble), we can create a migration as is, just copying its content. ```bash # Create new schema file touch apps/web/supabase/schemas/15-my-new-feature.sql # Create Migration pnpm --filter web run supabase migrations new my-new-feature # Copy content to migration cp apps/web/supabase/schemas/15-my-new-feature.sql apps/web/supabase/migrations/$(ls -t apps/web/supabase/migrations/ | head -n1) # Apply migration pnpm --filter web supabase migrations up # alternatively reset db with pnpm supabase:web:reset # Generate TypeScript types pnpm supabase:web:typegen ``` ### 2. Modifying existing entities When modifying existing entities (such ass adding a field to an existing table), we can use the `diff` command to generate a migration following the changes: ```bash # Edit schema file (e.g., schemas/03-accounts.sql) # Make your changes... # Create migration for changes pnpm --filter web run supabase:db:diff -f update-accounts # Apply and test pnpm --filter web supabase migrations up # alternatively reset db with pnpm supabase:web:reset # After resetting pnpm supabase:web:typegen ``` ## Security First Patterns ## Add permissions (if any) ```sql ALTER TYPE public.app_permissions ADD VALUE 'notes.manage'; COMMIT; ``` ### Table Creation with RLS ```sql -- Create table create table if not exists public.notes ( id uuid unique not null default extensions.uuid_generate_v4(), account_id uuid references public.accounts(id) on delete cascade not null, -- ... primary key (id) ); -- CRITICAL: Always enable RLS alter table "public"."notes" enable row level security; -- Revoke default permissions revoke all on public.notes from authenticated, service_role; -- Grant specific permissions grant select, insert, update, delete on table public.notes to authenticated; -- Add RLS policies create policy "notes_read" on public.notes for select to authenticated using ( account_id = (select auth.uid()) or public.has_role_on_account(account_id) ); create policy "notes_write" on public.notes for insert to authenticated with check ( public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions) ); create policy "notes_update" on public.notes for update to authenticated using ( public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions) ) with check ( public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions) ); create policy "notes_delete" on public.notes for delete to authenticated using ( public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions) ); ``` ### Storage Bucket Policies ```sql -- Create storage bucket insert into storage.buckets (id, name, public) values ('documents', 'documents', false); -- RLS policy for storage create policy documents_policy on storage.objects for all using ( bucket_id = 'documents' and ( -- File belongs to user's account kit.get_storage_filename_as_uuid(name) = auth.uid() or -- User has access to the account public.has_role_on_account(kit.get_storage_filename_as_uuid(name)) ) ) with check ( bucket_id = 'documents' and ( kit.get_storage_filename_as_uuid(name) = auth.uid() or public.has_permission( auth.uid(), kit.get_storage_filename_as_uuid(name), 'files.upload'::app_permissions ) ) ); ``` ## Function Creation Patterns ### Safe Security Definer Functions ```sql -- NEVER create security definer functions without explicit access controls create or replace function public.create_team_account(account_name text) returns public.accounts language plpgsql security definer -- Elevated privileges set search_path = '' -- Prevent SQL injection as $$ declare new_account public.accounts; begin -- CRITICAL: Validate permissions first if not public.is_set('enable_team_accounts') then raise exception 'Team accounts are not enabled'; end if; -- Additional validation can go here if length(account_name) < 3 then raise exception 'Account name must be at least 3 characters'; end if; -- Now safe to proceed with elevated privileges insert into public.accounts (name, is_personal_account) values (account_name, false) returning * into new_account; return new_account; end; $$; -- Grant to authenticated users only grant execute on function public.create_team_account(text) to authenticated; ``` ### Security Invoker Functions (Safer) ```sql -- Preferred: Functions that inherit RLS policies create or replace function public.get_account_notes(target_account_id uuid) returns setof public.notes language plpgsql security invoker -- Inherits caller's permissions (RLS applies) set search_path = '' as $$ begin -- RLS policies will automatically restrict results return query select * from public.notes where account_id = target_account_id order by created_at desc; end; $$; grant execute on function public.get_account_notes(uuid) to authenticated; ``` ### Safe Column Additions ```sql -- Safe: Add nullable columns alter table public.accounts add column if not exists description text; -- Safe: Add columns with defaults alter table public.accounts add column if not exists is_verified boolean default false not null; -- Unsafe: Adding non-null columns without defaults -- alter table public.accounts add column required_field text not null; -- DON'T DO THIS ``` ### Index Management ```sql -- Create indexes concurrently for large tables create index concurrently if not exists ix_accounts_created_at on public.accounts (created_at desc); -- Drop unused indexes drop index if exists ix_old_unused_index; ``` ## Testing Database Changes ### Local Testing ```bash # Test with fresh database pnpm supabase:web:reset # Test your changes pnpm run supabase:web:test ``` ## Common Schema Patterns ### Audit Trail Add triggers if the properties exist and are appropriate: - `public.trigger_set_timestamps()` - for tables with `created_at` and `updated_at` columns - `public.trigger_set_user_tracking()` - for tables with `created_by` and `updated_by` columns ### Useful Commands ```bash # View migration status pnpm --filter web supabase migrations list # Reset database completely pnpm supabase:web:reset # Generate migration from schema diff pnpm --filter web run supabase:db:diff -f migration-name ## Apply created migration pnpm --filter web supabase migrations up # Apply specific migration pnpm --filter web supabase migrations up --include-schemas public ```