Files
myeasycms-v2/apps/web/supabase/AGENTS.md
Giancarlo Buomprisco 533dfba5b9 Optimized agents rules subfolders, dependencies updates (#355)
* 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...)
2025-09-17 11:36:02 +08:00

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 with created_at and updated_at columns
  • public.trigger_set_user_tracking() - for tables with created_by and updated_by columns

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