Files
myeasycms-v2/packages/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

312 lines
9.1 KiB
Markdown

# Database & Authentication Instructions
This file contains instructions for working with Supabase, database security, and authentication.
## Database Security Guidelines ⚠️
**Critical Security Guidelines - Read Carefully!**
### Database Security Fundamentals
- **Always enable RLS** on new tables unless explicitly instructed otherwise
- **NEVER use SECURITY DEFINER functions** without explicit access controls - they bypass RLS entirely
- **Always use security_invoker=true for views** to maintain proper access control
- **Storage buckets MUST validate access** using account_id in the path structure. See `apps/web/supabase/schemas/16-storage.sql` for proper implementation.
- **Use locks if required**: Database locks prevent race conditions and timing attacks in concurrent operations. Make sure to take these into account for all database operations.
### Security Definer Function - Dangerous Pattern ❌
```sql
-- NEVER DO THIS - Allows any authenticated user to call function
CREATE OR REPLACE FUNCTION public.dangerous_function()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER AS $
BEGIN
-- This bypasses all RLS policies!
DELETE FROM sensitive_table; -- Anyone can call this!
END;
$;
GRANT EXECUTE ON FUNCTION public.dangerous_function() TO authenticated;
```
### Security Definer Function - Safe Pattern ✅
```sql
-- ONLY use SECURITY DEFINER with explicit access validation
CREATE OR REPLACE FUNCTION public.safe_admin_function(target_account_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = '' AS $
BEGIN
-- MUST validate caller has permission FIRST
IF NOT public.is_account_owner(target_account_id) THEN
RAISE EXCEPTION 'Access denied: insufficient permissions';
END IF;
-- Now safe to proceed with elevated privileges
-- Your admin operation here
END;
$;
```
Only grant critical functions to `service_role`:
```sql
grant execute on public.dangerous_function to service_role;
```
## Existing Helper Functions - Use These! 📚
**DO NOT recreate these functions - they already exist:**
```sql
-- Account Access Control
public.has_role_on_account(account_id, role?) -- Check team membership
public.has_permission(user_id, account_id, permission) -- Check permissions
public.is_account_owner(account_id) -- Verify ownership
public.has_active_subscription(account_id) -- Subscription status
public.is_team_member(account_id, user_id) -- Direct membership check
public.can_action_account_member(target_account_id, target_user_id) -- Member action rights
-- Administrative Functions
public.is_super_admin() -- Super admin check
public.is_aal2() -- MFA verification
public.is_mfa_compliant() -- MFA compliance
-- Configuration
public.is_set(field_name) -- Feature flag checks
```
Always check `apps/web/supabase/schemas/` before creating new functions!
## RLS Policy Best Practices ✅
```sql
-- Proper RLS using existing helper functions
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)
);
-- For operations requiring specific permissions
CREATE POLICY "notes_manage" ON public.notes FOR ALL
TO authenticated USING (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
## Schema Management Workflow
1. Create schemas in `apps/web/supabase/schemas/` as `<number>-<name>.sql`
2. After changes: `pnpm supabase:web:stop`
3. Run: `pnpm --filter web run supabase:db:diff -f <filename>`
4. Restart: `pnpm supabase:web:start` and `pnpm supabase:web:reset`
5. Generate types: `pnpm supabase:web:typegen`
- **Never modify database.types.ts**: Instead, use the Supabase CLI using our package.json scripts to re-generate the types after resetting the DB
### Key Schema Files
- Accounts: `apps/web/supabase/schemas/03-accounts.sql`
- Memberships: `apps/web/supabase/schemas/05-memberships.sql`
- Permissions: `apps/web/supabase/schemas/06-roles-permissions.sql`
## Type Generation
```typescript
import { Tables } from '@kit/supabase/database';
type Account = Tables<'accounts'>;
```
Always prefer inferring types from generated Database types.
## Client Usage Patterns
### 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} />;
}
```
### Client Components
```typescript
'use client';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
function InteractiveNotes() {
const supabase = useSupabase();
// Use with React Query for optimal data fetching
}
```
### Admin Client (Use with Extreme Caution) ⚠️
```typescript
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function adminFunction() {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient.from('table').select('*');
}
```
## Authentication Patterns
### Multi-Factor Authentication
```typescript
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
const requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(supabase);
if (requiresMultiFactorAuthentication) {
// Redirect to MFA page
}
```
### User Requirements
```typescript
import { requireUser } from '@kit/supabase/require-user';
const client = getSupabaseServerClient();
const user = await requireUser(client, { verifyMfa: false });
```
## Storage Security
Storage buckets must validate access using account_id in the path structure:
```sql
-- RLS policies for storage bucket account_image
create policy account_image on storage.objects for all using (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'settings.manage'
)
)
);
```
## Common Database Operations
### Creating Tables 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,
title varchar(255) not null,
content text,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
primary key (id)
);
-- Enable RLS
alter table "public"."notes" enable row level security;
-- Grant permissions
grant select, insert, update, delete on table public.notes to authenticated;
-- Create 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 (
account_id = (select auth.uid()) or
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
### Indexes for Performance
```sql
-- Create indexes for common queries
create index if not exists ix_notes_account_id on public.notes (account_id);
create index if not exists ix_notes_created_at on public.notes (created_at);
```
## Error Handling
```typescript
import { getLogger } from '@kit/shared/logger';
async function databaseOperation() {
const logger = await getLogger();
const ctx = { name: 'database-operation', accountId: 'account-123' };
try {
logger.info(ctx, 'Starting database operation');
const result = await client.from('table').select('*');
if (result.error) {
logger.error({ ...ctx, error: result.error }, 'Database query failed');
throw result.error;
}
return result.data;
} catch (error) {
logger.error({ ...ctx, error }, 'Database operation failed');
throw error;
}
}
```
## Migration Best Practices
1. Always test migrations locally first
2. Use transactions for complex migrations
3. Add proper indexes for new columns
4. Update RLS policies when adding new tables
5. Generate TypeScript types after schema changes
6. Take into account constraints
7. Do not add breaking changes that would distrupt the DB to new migrations
## Common Gotchas
1. **RLS bypass**: Admin client bypasses all RLS - validate manually
2. **Missing indexes**: Always add indexes for foreign keys and commonly queried columns
3. **Security definer functions**: Only use with explicit permission checks
4. **Storage paths**: Must include account_id for proper access control
5. **Type safety**: Always regenerate types after schema changes