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...)
This commit is contained in:
Giancarlo Buomprisco
2025-09-17 11:36:02 +08:00
committed by GitHub
parent 9fae142f2d
commit 533dfba5b9
83 changed files with 9223 additions and 2974 deletions

292
apps/web/supabase/AGENTS.md Normal file
View File

@@ -0,0 +1,292 @@
# 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
```

292
apps/web/supabase/CLAUDE.md Normal file
View File

@@ -0,0 +1,292 @@
# 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
```

View File

@@ -0,0 +1,42 @@
-- Triggers for accounts table
create trigger accounts_set_timestamps
before insert or update on public.accounts
for each row execute function public.trigger_set_timestamps();
create trigger accounts_set_user_tracking
before insert or update on public.accounts
for each row execute function public.trigger_set_user_tracking();
-- Triggers for accounts_memberships table
create trigger accounts_memberships_set_timestamps
before insert or update on public.accounts_memberships
for each row execute function public.trigger_set_timestamps();
create trigger accounts_memberships_set_user_tracking
before insert or update on public.accounts_memberships
for each row execute function public.trigger_set_user_tracking();
-- Triggers for invitations table
create trigger invitations_set_timestamps
before insert or update on public.invitations
for each row execute function public.trigger_set_timestamps();
-- Triggers for subscriptions table
create trigger subscriptions_set_timestamps
before insert or update on public.subscriptions
for each row execute function public.trigger_set_timestamps();
-- Triggers for subscription_items table
create trigger subscription_items_set_timestamps
before insert or update on public.subscription_items
for each row execute function public.trigger_set_timestamps();
-- Triggers for orders table
create trigger orders_set_timestamps
before insert or update on public.orders
for each row execute function public.trigger_set_timestamps();
-- Triggers for order_items table
create trigger order_items_set_timestamps
before insert or update on public.order_items
for each row execute function public.trigger_set_timestamps();

View File

@@ -77,6 +77,15 @@ create unique index unique_personal_account on public.accounts (primary_owner_us
where
is_personal_account = true;
-- Triggers for accounts table
create trigger accounts_set_timestamps
before insert or update on public.accounts
for each row execute function public.trigger_set_timestamps();
create trigger accounts_set_user_tracking
before insert or update on public.accounts
for each row execute function public.trigger_set_user_tracking();
-- RLS on the accounts table
-- UPDATE(accounts):
-- Team owners can update their accounts

View File

@@ -46,6 +46,15 @@ create index ix_accounts_memberships_user_id on public.accounts_memberships (use
create index ix_accounts_memberships_account_role on public.accounts_memberships (account_role);
-- Triggers for accounts_memberships table
create trigger accounts_memberships_set_timestamps
before insert or update on public.accounts_memberships
for each row execute function public.trigger_set_timestamps();
create trigger accounts_memberships_set_user_tracking
before insert or update on public.accounts_memberships
for each row execute function public.trigger_set_user_tracking();
-- Enable RLS on the accounts_memberships table
alter table public.accounts_memberships enable row level security;

View File

@@ -36,6 +36,11 @@ comment on column public.invitations.email is 'The email of the user being invit
-- Indexes on the invitations table
create index ix_invitations_account_id on public.invitations (account_id);
-- Triggers for invitations table
create trigger invitations_set_timestamps
before insert or update on public.invitations
for each row execute function public.trigger_set_timestamps();
-- Revoke all on invitations table from authenticated and service_role
revoke all on public.invitations
from

View File

@@ -69,6 +69,11 @@ select
-- Indexes on the subscriptions table
create index ix_subscriptions_account_id on public.subscriptions (account_id);
-- Triggers for subscriptions table
create trigger subscriptions_set_timestamps
before insert or update on public.subscriptions
for each row execute function public.trigger_set_timestamps();
-- Enable RLS on subscriptions table
alter table public.subscriptions enable row level security;
@@ -314,6 +319,11 @@ delete on table public.subscription_items to service_role;
-- Indexes on the subscription_items table
create index ix_subscription_items_subscription_id on public.subscription_items (subscription_id);
-- Triggers for subscription_items table
create trigger subscription_items_set_timestamps
before insert or update on public.subscription_items
for each row execute function public.trigger_set_timestamps();
-- RLS
alter table public.subscription_items enable row level security;

View File

@@ -55,6 +55,11 @@ delete on table public.orders to service_role;
-- Indexes on the orders table
create index ix_orders_account_id on public.orders (account_id);
-- Triggers for orders table
create trigger orders_set_timestamps
before insert or update on public.orders
for each row execute function public.trigger_set_timestamps();
-- RLS
alter table public.orders enable row level security;
@@ -130,6 +135,11 @@ grant insert, update, delete on table public.order_items to service_role;
-- Indexes on the order_items table
create index ix_order_items_order_id on public.order_items (order_id);
-- Triggers for order_items table
create trigger order_items_set_timestamps
before insert or update on public.order_items
for each row execute function public.trigger_set_timestamps();
-- RLS
alter table public.order_items enable row level security;

View File

@@ -0,0 +1,155 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select plan(12);
--- Test the trigger_set_timestamps function on all tables
--- This test verifies that created_at and updated_at are properly set on insert
--- Create test users
select tests.create_supabase_user('trigger_test_user1', 'test1@example.com');
-- Authenticate as test user
select makerkit.authenticate_as('trigger_test_user1');
------------
--- Test accounts table timestamp triggers - INSERT
------------
INSERT INTO public.accounts (name, is_personal_account)
VALUES ('Test Account', false);
SELECT ok(
(SELECT created_at IS NOT NULL FROM public.accounts WHERE name = 'Test Account'),
'accounts: created_at should be set automatically on insert'
);
SELECT ok(
(SELECT updated_at IS NOT NULL FROM public.accounts WHERE name = 'Test Account'),
'accounts: updated_at should be set automatically on insert'
);
SELECT ok(
(SELECT created_at = updated_at FROM public.accounts WHERE name = 'Test Account'),
'accounts: created_at should equal updated_at on insert'
);
------------
--- Test invitations table timestamp triggers - INSERT
------------
-- Create a team account for invitation testing
INSERT INTO public.accounts (name, is_personal_account)
VALUES ('Invitation Test Team', false);
-- Test invitation insert
INSERT INTO public.invitations (email, account_id, invited_by, role, invite_token, expires_at)
VALUES (
'invitee@example.com',
(SELECT id FROM public.accounts WHERE name = 'Invitation Test Team'),
tests.get_supabase_uid('trigger_test_user1'),
'member',
'test-token-123',
now() + interval '7 days'
);
SELECT ok(
(SELECT created_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'),
'invitations: created_at should be set automatically on insert'
);
SELECT ok(
(SELECT updated_at IS NOT NULL FROM public.invitations WHERE email = 'invitee@example.com'),
'invitations: updated_at should be set automatically on insert'
);
SELECT ok(
(SELECT created_at = updated_at FROM public.invitations WHERE email = 'invitee@example.com'),
'invitations: created_at should equal updated_at on insert'
);
------------
--- Test subscriptions table timestamp triggers - INSERT (service_role required)
------------
set role service_role;
-- Create billing customer first
INSERT INTO public.billing_customers (account_id, provider, customer_id, email)
VALUES (
(SELECT id FROM public.accounts WHERE name = 'Invitation Test Team'),
'stripe',
'cus_test123',
'billing@example.com'
);
-- Test subscription insert
INSERT INTO public.subscriptions (
id, account_id, billing_customer_id, status, active, billing_provider,
cancel_at_period_end, currency, period_starts_at, period_ends_at
)
VALUES (
'sub_test123',
(SELECT id FROM public.accounts WHERE name = 'Invitation Test Team'),
(SELECT id FROM public.billing_customers WHERE customer_id = 'cus_test123'),
'active',
true,
'stripe',
false,
'USD',
now(),
now() + interval '1 month'
);
SELECT ok(
(SELECT created_at IS NOT NULL FROM public.subscriptions WHERE id = 'sub_test123'),
'subscriptions: created_at should be set automatically on insert'
);
SELECT ok(
(SELECT updated_at IS NOT NULL FROM public.subscriptions WHERE id = 'sub_test123'),
'subscriptions: updated_at should be set automatically on insert'
);
SELECT ok(
(SELECT created_at = updated_at FROM public.subscriptions WHERE id = 'sub_test123'),
'subscriptions: created_at should equal updated_at on insert'
);
------------
--- Test subscription_items table timestamp triggers - INSERT
------------
-- Test subscription_item insert
INSERT INTO public.subscription_items (
id, subscription_id, product_id, variant_id, type, quantity, interval, interval_count
)
VALUES (
'si_test123',
'sub_test123',
'prod_test123',
'var_test123',
'flat',
1,
'month',
1
);
SELECT ok(
(SELECT created_at IS NOT NULL FROM public.subscription_items WHERE id = 'si_test123'),
'subscription_items: created_at should be set automatically on insert'
);
SELECT ok(
(SELECT updated_at IS NOT NULL FROM public.subscription_items WHERE id = 'si_test123'),
'subscription_items: updated_at should be set automatically on insert'
);
SELECT ok(
(SELECT created_at = updated_at FROM public.subscription_items WHERE id = 'si_test123'),
'subscription_items: created_at should equal updated_at on insert'
);
SELECT * FROM finish();
ROLLBACK;

View File

@@ -0,0 +1,43 @@
BEGIN;
create extension "basejump-supabase_test_helpers" version '0.0.6';
select plan(3);
--- Test the trigger_set_user_tracking function on accounts table
--- This test verifies that created_by and updated_by are properly set on insert
--- Create test users
select tests.create_supabase_user('user_tracking_test1', 'tracking1@example.com');
------------
--- Test accounts table user tracking triggers - INSERT
------------
-- Authenticate as first user for insert
select makerkit.authenticate_as('user_tracking_test1');
-- Test INSERT: created_by and updated_by should be set to current user
INSERT INTO public.accounts (name, is_personal_account)
VALUES ('User Tracking Test Account', false);
SELECT ok(
(SELECT created_by = tests.get_supabase_uid('user_tracking_test1')
FROM public.accounts WHERE name = 'User Tracking Test Account'),
'accounts: created_by should be set to current user on insert'
);
SELECT ok(
(SELECT updated_by = tests.get_supabase_uid('user_tracking_test1')
FROM public.accounts WHERE name = 'User Tracking Test Account'),
'accounts: updated_by should be set to current user on insert'
);
SELECT ok(
(SELECT created_by = updated_by
FROM public.accounts WHERE name = 'User Tracking Test Account'),
'accounts: created_by should equal updated_by on insert'
);
SELECT * FROM finish();
ROLLBACK;