# Database Patterns - CareBridge ## Overview CareBridge uses Supabase (PostgreSQL) for data storage. Security is enforced via **Clerk authentication + Server Actions** for most tables, with RLS enabled only for specific tables (user_profiles, consultations, subscriptions). ## Database Schema Reference ### Auto-Generated Types (Single Source of Truth) The complete database schema is auto-generated in **`src/lib/database.types.ts`**. **To regenerate types after migrations:** ```bash # From linked remote project (recommended) npx supabase gen types typescript --linked > src/lib/database.types.ts # From local dev database npx supabase gen types typescript --local > src/lib/database.types.ts # From specific project ID npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/lib/database.types.ts ``` **ALWAYS regenerate types after creating migrations!** ## Supabase Client Patterns ### Server-Side Client (Preferred) Use in Server Components and Server Actions: ```typescript import { createServerClient } from '@/lib/supabase/server' export default async function Page() { const supabase = await createServerClient() const { data, error } = await supabase .from('cases') .select('*') .eq('owner_id', userId) if (error) { console.error('Database error:', error) return } return } ``` ### Client-Side Client (When Needed) Use in Client Components: ```typescript 'use client' import { createBrowserClient } from '@/lib/supabase/client' import { useEffect, useState } from 'react' export function ClientComponent() { const [data, setData] = useState(null) const supabase = createBrowserClient() useEffect(() => { async function fetchData() { const { data } = await supabase.from('cases').select('*') setData(data) } fetchData() }, []) // ... } ``` ## Migrations ### Creating Migrations **ALWAYS use Supabase CLI for schema changes:** ```bash # Create a new migration npx supabase migration new migration_name # This creates: supabase/migrations/YYYYMMDD_migration_name.sql ``` ### Migration Structure ```sql -- Migration: 20251018_add_concierge_packages.sql -- Create table CREATE TABLE IF NOT EXISTS concierge_packages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), clerk_user_id TEXT NOT NULL, package_type VARCHAR(50) NOT NULL, price_paid_cents INT NOT NULL CHECK (price_paid_cents > 0), status VARCHAR(20) DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- Create indexes CREATE INDEX idx_concierge_packages_user ON concierge_packages(clerk_user_id); CREATE INDEX idx_concierge_packages_status ON concierge_packages(status); -- Add table comment COMMENT ON TABLE concierge_packages IS 'Stores concierge service package purchases. Security enforced by Clerk auth + Server Actions (RLS disabled)'; -- Create trigger for updated_at (if function exists) CREATE TRIGGER update_concierge_packages_updated_at BEFORE UPDATE ON concierge_packages FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Add column comments COMMENT ON COLUMN concierge_packages.price_paid_cents IS 'Price paid in cents (e.g., 39900 = $399.00)'; ``` ### Applying Migrations ```bash # Apply locally npx supabase db reset # Apply to remote (production) npx supabase db push ``` ## Security Architecture ### CareBridge Uses Two Security Patterns: **Pattern 1: Clerk Authentication + Server Actions (Most Tables)** Most tables have RLS **disabled** and rely on: - Clerk authentication for user identity - Server Actions that enforce authorization logic - Server-side validation before database operations ```sql -- Example: Most tables use this pattern CREATE TABLE cases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id TEXT NOT NULL, -- ... other columns ); -- RLS is DISABLED - security enforced in Server Actions COMMENT ON TABLE cases IS 'Security enforced by Clerk auth + Server Actions (RLS disabled)'; ``` **Pattern 2: Row-Level Security (Specific Tables Only)** Only 3 tables use RLS: - `user_profiles` - User profile data - `consultations` - Consultation bookings - `subscriptions` - Subscription data ```sql -- Example: Tables with RLS enabled ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY; CREATE POLICY "users_own_profile" ON user_profiles FOR ALL USING (auth.jwt() ->> 'sub' = clerk_user_id); ``` ### When to Use Each Pattern **Use Clerk + Server Actions (default):** - Complex authorization logic (team access, roles) - Multi-table operations requiring consistency - Business logic validation - Most application features **Use RLS (rare):** - Simple user-owned resources - Direct client queries (rare in Next.js) - Additional security layer for sensitive data ### Server Action Security Pattern ```typescript // src/lib/actions/case-actions.ts 'use server' import { auth } from '@clerk/nextjs/server' import { createServerClient } from '@/lib/supabase/server' export async function getCases() { const { userId } = await auth() if (!userId) { throw new Error('Unauthorized') } const supabase = await createServerClient() // Security: Only fetch cases owned by authenticated user const { data, error } = await supabase .from('cases') .select('*') .eq('owner_id', userId) if (error) throw error return data } ``` ## Query Patterns ### Basic CRUD ```typescript // Create const { data, error } = await supabase .from('cases') .insert({ owner_id: userId, care_recipient_name: 'John Doe', }) .select() .single() // Read (single) const { data, error } = await supabase .from('cases') .select('*') .eq('id', caseId) .single() // Read (multiple) const { data, error } = await supabase .from('cases') .select('*') .eq('owner_id', userId) // Update const { data, error } = await supabase .from('cases') .update({ care_recipient_name: 'Jane Doe' }) .eq('id', caseId) // Delete const { data, error } = await supabase .from('cases') .delete() .eq('id', caseId) ``` ### Joins (Relations) ```typescript // Get cases with their members const { data, error } = await supabase .from('cases') .select(` *, case_members ( id, user_id, role ) `) .eq('owner_id', userId) ``` ### Filtering ```typescript // Equals .eq('status', 'active') // Not equals .neq('status', 'deleted') // Greater than .gt('created_at', '2024-01-01') // In array .in('status', ['active', 'pending']) // Is null .is('deleted_at', null) // Like (case-insensitive) .ilike('name', '%john%') // Order .order('created_at', { ascending: false }) // Limit .limit(10) // Range (pagination) .range(0, 9) // First 10 items ``` ### Counting ```typescript const { count, error } = await supabase .from('cases') .select('*', { count: 'exact', head: true }) .eq('owner_id', userId) ``` ### Upsert (Insert or Update) ```typescript const { data, error } = await supabase .from('subscriptions') .upsert({ clerk_user_id: userId, is_active: true, }) .select() ``` ## Transaction Patterns ### Using RPC for Transactions Create a database function: ```sql -- supabase/migrations/20251018_create_case_with_member.sql CREATE OR REPLACE FUNCTION create_case_with_member( p_owner_id TEXT, p_case_name TEXT ) RETURNS UUID AS $$ DECLARE v_case_id UUID; BEGIN -- Insert case INSERT INTO cases (owner_id, care_recipient_name) VALUES (p_owner_id, p_case_name) RETURNING id INTO v_case_id; -- Insert owner as member INSERT INTO case_members (case_id, user_id, role) VALUES (v_case_id, p_owner_id, 'owner'); RETURN v_case_id; END; $$ LANGUAGE plpgsql; ``` Call from TypeScript: ```typescript const { data, error } = await supabase.rpc('create_case_with_member', { p_owner_id: userId, p_case_name: 'John Doe', }) ``` ## Error Handling ```typescript const { data, error } = await supabase .from('cases') .select('*') .eq('id', caseId) .single() if (error) { console.error('Database error:', error) // Check for specific error codes if (error.code === 'PGRST116') { // Not found return { error: 'Case not found' } } return { error: 'Failed to fetch case' } } return { data } ``` ## Type Safety with Auto-Generated Types CareBridge uses auto-generated types from `src/lib/database.types.ts`: ```typescript import { Database } from '@/lib/database.types' // Extract table types type Case = Database['public']['Tables']['cases']['Row'] type CaseInsert = Database['public']['Tables']['cases']['Insert'] type CaseUpdate = Database['public']['Tables']['cases']['Update'] // Use with Supabase client const supabase = createServerClient() // Typed queries const { data, error } = await supabase .from('cases') // ✅ Autocomplete for table names .select('*') // ✅ Typed return value .eq('id', caseId) ``` **Remember to regenerate types after migrations:** ```bash npx supabase gen types typescript --linked > src/lib/database.types.ts ``` ## Realtime Subscriptions ```typescript 'use client' import { useEffect } from 'react' import { createBrowserClient } from '@/lib/supabase/client' export function RealtimeComponent() { const supabase = createBrowserClient() useEffect(() => { const channel = supabase .channel('cases-changes') .on( 'postgres_changes', { event: '*', schema: 'public', table: 'cases', }, (payload) => { console.log('Change received!', payload) // Update UI } ) .subscribe() return () => { supabase.removeChannel(channel) } }, []) return
Listening for changes...
} ``` ## Common Mistakes ### ❌ Mistake #1: Not checking for errors ```typescript // WRONG - No error handling const { data } = await supabase.from('cases').select('*') return data // Could be null! // CORRECT const { data, error } = await supabase.from('cases').select('*') if (error) { console.error('Error:', error) return { error: error.message } } return { data } ``` ### ❌ Mistake #2: Using client in server components ```typescript // WRONG - Don't import client in server components import { createBrowserClient } from '@/lib/supabase/client' export default async function Page() { const supabase = createBrowserClient() // ERROR! } // CORRECT - Use server client import { createServerClient } from '@/lib/supabase/server' export default async function Page() { const supabase = await createServerClient() } ``` ### ❌ Mistake #3: Not securing Server Actions ```typescript // WRONG - No authentication check 'use server' export async function deleteCase(caseId: string) { const supabase = await createServerClient() await supabase.from('cases').delete().eq('id', caseId) // Anyone can delete any case! } // CORRECT - Verify user owns the case 'use server' import { auth } from '@clerk/nextjs/server' export async function deleteCase(caseId: string) { const { userId } = await auth() if (!userId) { throw new Error('Unauthorized') } const supabase = await createServerClient() // Verify ownership before deleting const { data: caseData } = await supabase .from('cases') .select('owner_id') .eq('id', caseId) .single() if (caseData?.owner_id !== userId) { throw new Error('Unauthorized') } await supabase.from('cases').delete().eq('id', caseId) } ``` ### ❌ Mistake #4: Not using indexes ```sql -- WRONG - Frequently queried column without index CREATE TABLE cases ( id UUID PRIMARY KEY, owner_id TEXT NOT NULL -- No index! ); -- CORRECT - Add indexes for frequently queried columns CREATE TABLE cases ( id UUID PRIMARY KEY, owner_id TEXT NOT NULL ); CREATE INDEX idx_cases_owner ON cases(owner_id); ``` ### ❌ Mistake #5: Not using migrations ```typescript // WRONG - Creating tables via SQL in application code await supabase.sql`CREATE TABLE ...` // Don't do this! // CORRECT - Use migrations // Create: supabase/migrations/20251018_create_table.sql // Then run: npx supabase db push ``` ## Performance Tips 1. **Use indexes** for frequently queried columns 2. **Select specific columns** instead of `select('*')` 3. **Use pagination** with `.range()` for large datasets 4. **Batch operations** when possible 5. **Use RPC functions** for complex operations 6. **Cache results** when appropriate ## Migration Checklist When creating a migration: - [ ] Use `npx supabase migration new name` - [ ] Include `IF NOT EXISTS` clauses - [ ] Add appropriate indexes for frequently queried columns - [ ] Add table comment indicating security pattern (RLS vs Clerk + Server Actions) - [ ] Only enable RLS if needed (most tables use Clerk + Server Actions) - [ ] Add `updated_at` trigger if needed - [ ] Add column comments for documentation - [ ] Test locally with `npx supabase db reset` - [ ] Push to production with `npx supabase db push` - [ ] **Regenerate TypeScript types:** `npx supabase gen types typescript --linked > src/lib/database.types.ts`