1. Added Claude Code sub-agents 2. Added PRD tool to MCP Server 3. Added MCP Server UI to Dev Tools 4. Improved MCP Server Database Tool 5. Updated dependencies
266 lines
6.9 KiB
Markdown
266 lines
6.9 KiB
Markdown
# 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
|
|
```
|