* 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...)
293 lines
6.9 KiB
Markdown
293 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.
|
|
|
|
## Schema Development Workflow
|
|
|
|
### 1. Creating New Schema Files
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```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 supabase:web:reset
|
|
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
|
|
```
|
|
|
|
## Type Generation
|
|
|
|
### After Schema Changes
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```typescript
|
|
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 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 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
|
|
```
|