* Update AGENTS.md and CLAUDE.md for improved clarity and structure * Added MCP Server * Added missing triggers to tables that should have used them * Updated all dependencies * Fixed rare bug in React present in the Admin layout which prevents navigating to pages (sometimes...)
6.9 KiB
6.9 KiB
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.
Schema Development Workflow
1. Creating New Schema Files
# Create new schema file
touch schemas/15-my-new-feature.sql
# Apply changes and create migration
pnpm --filter web run supabase:db:diff -f my-new-feature
# Restart Supabase with fresh schema
pnpm supabase:web:reset
# Generate TypeScript types
pnpm supabase:web:typegen
2. Modifying Existing Schemas
# 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 supabase:web:reset
pnpm supabase:web:typegen
Security First Patterns
Add permissions (if any)
ALTER TYPE public.app_permissions ADD VALUE 'notes.manage';
COMMIT;
Table Creation with RLS
-- 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
-- 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
-- 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)
-- 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
-- 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
-- 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
# Test with fresh database
pnpm supabase:web:reset
# Test your changes
pnpm run supabase:web:test
Type Generation
After Schema Changes
# Generate types after any schema changes
pnpm supabase:web:typegen
# Types are generated to src/lib/supabase/database.types.ts
# Reset DB
pnpm supabase:web:reset
Using Generated Types
import { Enums, Tables } from '@kit/supabase/database';
// Table types
type Account = Tables<'accounts'>;
type Note = Tables<'notes'>;
// Enum types
type AppPermission = Enums<'app_permissions'>;
// Insert types
type AccountInsert = Tables<'accounts'>['Insert'];
type AccountUpdate = Tables<'accounts'>['Update'];
// Use in functions
async function createNote(data: Tables<'notes'>['Insert']) {
const { data: note, error } = await supabase
.from('notes')
.insert(data)
.select()
.single();
return note;
}
Common Schema Patterns
Audit Trail
Add triggers if the properties exist and are appropriate:
public.trigger_set_timestamps()- for tables withcreated_atandupdated_atcolumnspublic.trigger_set_user_tracking()- for tables withcreated_byandupdated_bycolumns
Useful Commands
# View migration status
pnpm --filter web supabase migration list
# Reset database completely
pnpm supabase:web:reset
# Generate migration from schema diff
pnpm --filter web run supabase:db:diff -f migration-name
# Apply specific migration
pnpm --filter web supabase migration up --include-schemas public