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

264
apps/web/AGENTS.md Normal file
View File

@@ -0,0 +1,264 @@
# Web Application Instructions
This file contains instructions specific to the main Next.js web application.
## Application Structure
### Route Organization
```
app/
├── (marketing)/ # Public pages (landing, blog, docs)
├── (auth)/ # Authentication pages
├── home/
│ ├── (user)/ # Personal account context
│ └── [account]/ # Team account context ([account] = team slug)
├── admin/ # Super admin section
└── api/ # API routes
```
Key Examples:
- Marketing layout: `app/(marketing)/layout.tsx`
- Personal dashboard: `app/home/(user)/page.tsx`
- Team workspace: `app/home/[account]/page.tsx`
- Admin section: `app/admin/page.tsx`
### Component Organization
- **Route-specific**: Use `_components/` directories
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
- **Global components**: Root-level directories
Example:
- Team components: `app/home/[account]/_components/`
- Team server utils: `app/home/[account]/_lib/server/`
- Marketing components: `app/(marketing)/_components/`
## Data Fetching Strategy
**Quick Decision Framework:**
- **Server Components**: Default choice for initial data loading
- **Client Components**: For interactive features requiring hooks or real-time updates
- **Admin Client**: Only for bypassing RLS (rare cases - requires manual auth/authorization)
### Server Components (Preferred) ✅
```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function NotesPage() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
```
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
### Client Components (Interactive) 🖱️
```typescript
'use client';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useQuery } from '@tanstack/react-query';
function InteractiveNotes() {
const supabase = useSupabase();
const { data, isLoading } = useQuery({
queryKey: ['notes'],
queryFn: () => supabase.from('notes').select('*')
});
if (isLoading) return <Spinner />;
return <NotesList notes={data} />;
}
```
### Performance Optimization - Parallel Data Fetching 🚀
**Sequential (Slow) Pattern ❌**
```typescript
async function SlowDashboard() {
const userData = await loadUserData();
const notifications = await loadNotifications();
const metrics = await loadMetrics();
// Total time: sum of all requests
}
```
**Parallel (Optimized) Pattern ✅**
```typescript
async function FastDashboard() {
// Execute all requests simultaneously
const [userData, notifications, metrics] = await Promise.all([
loadUserData(),
loadNotifications(),
loadMetrics()
]);
// Total time: longest single request
return <Dashboard user={userData} notifications={notifications} metrics={metrics} />;
}
```
**Performance Impact**: Parallel fetching can reduce page load time by 60-80% for multi-data pages!
## Authorization Patterns - Critical Understanding 🔐
### RLS-Protected Data Fetching (Standard) ✅
```typescript
async function getUserNotes(userId: string) {
const client = getSupabaseServerClient();
// RLS automatically ensures user can only access their own notes
// NO additional authorization checks needed!
const { data } = await client.from('notes').select('*').eq('user_id', userId); // RLS validates this automatically
return data;
}
```
### Admin Client Usage (Dangerous - Rare Cases Only) ⚠️
```typescript
async function adminGetUserNotes(userId: string) {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Additional validation: ensure current admin isn't targeting themselves
if (currentUser.id === userId) {
throw new Error('Cannot perform admin action on own account');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient
.from('notes')
.select('*')
.eq('user_id', userId);
return data;
}
```
**Rule of thumb**: If using standard Supabase client, trust RLS. If using admin client, validate everything manually.
## Internationalization
Always use `Trans` component from `@kit/ui/trans`:
```tsx
import { Trans } from '@kit/ui/trans';
<Trans
i18nKey="user:welcomeMessage"
values={{ name: user.name }}
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
}}
/>
```
### Adding New Languages
1. Add language code to `lib/i18n/i18n.settings.ts`
2. Create translation files in `public/locales/[new-language]/`
3. Copy structure from English files
Translation files: `public/locales/<locale>/<namespace>.json`
## Workspace Contexts 🏢
### Personal Account Context (`app/home/(user)`)
```tsx
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
function PersonalComponent() {
const { user, account } = useUserWorkspace();
// Personal account data
}
```
Context provider: `@packages/features/accounts/src/components/user-workspace-context-provider.tsx`
### Team Account Context (`app/home/[account]`)
```tsx
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
function TeamComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// Team account data with permissions
}
```
Context provider: `@packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
## Key Configuration Files
- **Feature flags**: `config/feature-flags.config.ts`
- **i18n settings**: `lib/i18n/i18n.settings.ts`
- **Supabase config**: `supabase/config.toml`
- **Middleware**: `middleware.ts`
## Route Handlers (API Routes)
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`:
```typescript
import { enhanceRouteHandler } from '@kit/next/routes';
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// body is validated, user available if auth: true
return NextResponse.json({ success: true });
},
{
auth: true,
schema: ZodSchema,
},
);
```
## Security Guidelines 🛡️
### Authentication & Authorization
- Authentication already enforced by middleware
- Authorization handled by RLS at database level (in most cases)
- Avoid defensive code - use RLS instead
- When using the Supabase admin client, must enforce both authentication and authorization
### Passing data to the client
- **Never pass sensitive data** to Client Components
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
- Always validate user input
### Super Admin Protection
For admin routes, use `AdminGuard` from `@packages/features/admin/src/components/admin-guard.tsx`:
```tsx
import { AdminGuard } from '@kit/admin/components/admin-guard';
export default AdminGuard(AdminPageComponent);
```

264
apps/web/CLAUDE.md Normal file
View File

@@ -0,0 +1,264 @@
# Web Application Instructions
This file contains instructions specific to the main Next.js web application.
## Application Structure
### Route Organization
```
app/
├── (marketing)/ # Public pages (landing, blog, docs)
├── (auth)/ # Authentication pages
├── home/
│ ├── (user)/ # Personal account context
│ └── [account]/ # Team account context ([account] = team slug)
├── admin/ # Super admin section
└── api/ # API routes
```
Key Examples:
- Marketing layout: `app/(marketing)/layout.tsx`
- Personal dashboard: `app/home/(user)/page.tsx`
- Team workspace: `app/home/[account]/page.tsx`
- Admin section: `app/admin/page.tsx`
### Component Organization
- **Route-specific**: Use `_components/` directories
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
- **Global components**: Root-level directories
Example:
- Team components: `app/home/[account]/_components/`
- Team server utils: `app/home/[account]/_lib/server/`
- Marketing components: `app/(marketing)/_components/`
## Data Fetching Strategy
**Quick Decision Framework:**
- **Server Components**: Default choice for initial data loading
- **Client Components**: For interactive features requiring hooks or real-time updates
- **Admin Client**: Only for bypassing RLS (rare cases - requires manual auth/authorization)
### Server Components (Preferred) ✅
```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function NotesPage() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
```
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
### Client Components (Interactive) 🖱️
```typescript
'use client';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useQuery } from '@tanstack/react-query';
function InteractiveNotes() {
const supabase = useSupabase();
const { data, isLoading } = useQuery({
queryKey: ['notes'],
queryFn: () => supabase.from('notes').select('*')
});
if (isLoading) return <Spinner />;
return <NotesList notes={data} />;
}
```
### Performance Optimization - Parallel Data Fetching 🚀
**Sequential (Slow) Pattern ❌**
```typescript
async function SlowDashboard() {
const userData = await loadUserData();
const notifications = await loadNotifications();
const metrics = await loadMetrics();
// Total time: sum of all requests
}
```
**Parallel (Optimized) Pattern ✅**
```typescript
async function FastDashboard() {
// Execute all requests simultaneously
const [userData, notifications, metrics] = await Promise.all([
loadUserData(),
loadNotifications(),
loadMetrics()
]);
// Total time: longest single request
return <Dashboard user={userData} notifications={notifications} metrics={metrics} />;
}
```
**Performance Impact**: Parallel fetching can reduce page load time by 60-80% for multi-data pages!
## Authorization Patterns - Critical Understanding 🔐
### RLS-Protected Data Fetching (Standard) ✅
```typescript
async function getUserNotes(userId: string) {
const client = getSupabaseServerClient();
// RLS automatically ensures user can only access their own notes
// NO additional authorization checks needed!
const { data } = await client.from('notes').select('*').eq('user_id', userId); // RLS validates this automatically
return data;
}
```
### Admin Client Usage (Dangerous - Rare Cases Only) ⚠️
```typescript
async function adminGetUserNotes(userId: string) {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Additional validation: ensure current admin isn't targeting themselves
if (currentUser.id === userId) {
throw new Error('Cannot perform admin action on own account');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient
.from('notes')
.select('*')
.eq('user_id', userId);
return data;
}
```
**Rule of thumb**: If using standard Supabase client, trust RLS. If using admin client, validate everything manually.
## Internationalization
Always use `Trans` component from `@kit/ui/trans`:
```tsx
import { Trans } from '@kit/ui/trans';
<Trans
i18nKey="user:welcomeMessage"
values={{ name: user.name }}
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
}}
/>
```
### Adding New Languages
1. Add language code to `lib/i18n/i18n.settings.ts`
2. Create translation files in `public/locales/[new-language]/`
3. Copy structure from English files
Translation files: `public/locales/<locale>/<namespace>.json`
## Workspace Contexts 🏢
### Personal Account Context (`app/home/(user)`)
```tsx
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
function PersonalComponent() {
const { user, account } = useUserWorkspace();
// Personal account data
}
```
Context provider: `@packages/features/accounts/src/components/user-workspace-context-provider.tsx`
### Team Account Context (`app/home/[account]`)
```tsx
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
function TeamComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// Team account data with permissions
}
```
Context provider: `@packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
## Key Configuration Files
- **Feature flags**: `config/feature-flags.config.ts`
- **i18n settings**: `lib/i18n/i18n.settings.ts`
- **Supabase config**: `supabase/config.toml`
- **Middleware**: `middleware.ts`
## Route Handlers (API Routes)
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`:
```typescript
import { enhanceRouteHandler } from '@kit/next/routes';
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// body is validated, user available if auth: true
return NextResponse.json({ success: true });
},
{
auth: true,
schema: ZodSchema,
},
);
```
## Security Guidelines 🛡️
### Authentication & Authorization
- Authentication already enforced by middleware
- Authorization handled by RLS at database level (in most cases)
- Avoid defensive code - use RLS instead
- When using the Supabase admin client, must enforce both authentication and authorization
### Passing data to the client
- **Never pass sensitive data** to Client Components
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
- Always validate user input
### Super Admin Protection
For admin routes, use `AdminGuard` from `@packages/features/admin/src/components/admin-guard.tsx`:
```tsx
import { AdminGuard } from '@kit/admin/components/admin-guard';
export default AdminGuard(AdminPageComponent);
```

View File

@@ -0,0 +1,3 @@
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;

View File

@@ -32,7 +32,7 @@
},
"dependencies": {
"@edge-csrf/nextjs": "2.5.3-cloudflare-rc1",
"@hookform/resolvers": "^5.2.1",
"@hookform/resolvers": "^5.2.2",
"@kit/accounts": "workspace:*",
"@kit/admin": "workspace:*",
"@kit/analytics": "workspace:*",
@@ -53,15 +53,15 @@
"@kit/ui": "workspace:*",
"@makerkit/data-loader-supabase-core": "^0.0.10",
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
"@marsidev/react-turnstile": "^1.3.0",
"@marsidev/react-turnstile": "^1.3.1",
"@nosecone/next": "1.0.0-beta.11",
"@radix-ui/react-icons": "^1.3.2",
"@supabase/supabase-js": "2.57.2",
"@tanstack/react-query": "5.87.1",
"@supabase/supabase-js": "2.57.4",
"@tanstack/react-query": "5.89.0",
"@tanstack/react-table": "^8.21.3",
"date-fns": "^4.1.0",
"lucide-react": "^0.542.0",
"next": "15.5.2",
"lucide-react": "^0.544.0",
"next": "15.5.3",
"next-sitemap": "^4.2.3",
"next-themes": "0.4.6",
"react": "19.1.1",
@@ -76,16 +76,16 @@
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@next/bundle-analyzer": "15.5.2",
"@next/bundle-analyzer": "15.5.3",
"@tailwindcss/postcss": "^4.1.13",
"@types/node": "^24.3.1",
"@types/react": "19.1.12",
"@types/node": "^24.5.0",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"babel-plugin-react-compiler": "19.1.0-rc.3",
"cssnano": "^7.1.1",
"pino-pretty": "13.0.0",
"prettier": "^3.6.2",
"supabase": "2.39.2",
"supabase": "2.40.7",
"tailwindcss": "4.1.13",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.2"

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;