Awesome-omni-skill database-patterns
Supabase database query patterns and best practices for Splits Network
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/backend/database-patterns" ~/.claude/skills/diegosouzapw-awesome-omni-skill-database-patterns && rm -rf "$T"
skills/backend/database-patterns/SKILL.mdDatabase Patterns Skill
This skill provides guidance for working with the Supabase Postgres database in Splits Network services.
Purpose
Help developers write efficient, secure database queries following Splits Network standards:
- Access Context: Role-based data filtering
- Query Optimization: JOINs, indexes, and performance
- Migrations: Safe schema changes
- Transactions: Data consistency patterns
- Type Safety: Proper TypeScript integration
When to Use This Skill
Use this skill when:
- Writing repository methods
- Creating database migrations
- Optimizing query performance
- Implementing role-based data access
- Handling transactions
Core Principles
1. Repository Pattern with Access Context
All repository list methods must use access context for role-based filtering:
import { resolveAccessContext } from '@splits-network/shared-access-context'; import { SupabaseClient } from '@supabase/supabase-js'; export class JobRepository { constructor(private supabase: SupabaseClient) {} async list(clerkUserId: string, filters: JobFilters) { // Resolve user context (role, permissions, accessible entities) const context = await resolveAccessContext(clerkUserId, this.supabase); const query = this.supabase .from('jobs') .select('*'); // Apply role-based filtering if (context.role === 'recruiter') { // Recruiters see only assigned jobs const assignments = await this.getAssignments(context.userId); query.in('id', assignments.map(a => a.job_id)); } else if (context.isCompanyUser) { // Company users see their organization's jobs query.in('company_id', context.accessibleCompanyIds); } // Platform admins see everything (no filter) // Apply search/filter criteria if (filters.search) { query.ilike('title', `%${filters.search}%`); } if (filters.status) { query.eq('status', filters.status); } return query; } }
See examples/repository-with-access-context.ts for complete pattern.
2. Query Building Best Practices
Use proper SELECT patterns:
// ✅ CORRECT - Specific columns for large tables const { data } = await supabase .from('applications') .select('id, candidate_id, job_id, stage, created_at'); // ✅ CORRECT - Nested selects for related data const { data } = await supabase .from('applications') .select(` *, candidate:candidates(id, name, email), job:jobs(id, title, company_id) `); // ⚠️ USE SPARINGLY - Select * for small lookup tables const { data } = await supabase .from('job_statuses') .select('*'); // OK for small enum tables
See examples/query-building.ts for patterns.
3. JOIN Patterns for Data Enrichment
All tables are in
public schema - JOINs work seamlessly:
// Enrich applications with candidate and job data const { data } = await supabase .from('applications') .select(` *, candidate:candidates( id, name, email, user:users(clerk_user_id) ), job:jobs( id, title, status, company:companies(id, name) ), recruiter:recruiters( id, user:users(name, email) ) `) .eq('id', applicationId) .single();
Benefits:
- One query instead of N+1 queries
- Reduced API calls
- Consistent data shape
- Better performance
See examples/join-patterns.ts.
4. Filtering and Sorting
// Multiple filters query .eq('status', 'active') .gte('created_at', startDate) .lte('created_at', endDate) .in('company_id', [id1, id2]); // Text search (case-insensitive) query.ilike('title', `%${searchTerm}%`); // Sorting query.order('created_at', { ascending: false }); // Pagination query .range((page - 1) * limit, page * limit - 1);
See examples/filtering-and-sorting.ts.
5. Transaction Patterns
For multi-table operations that must succeed/fail together:
async createPlacement(data: PlacementCreate) { const { data: placement, error: placementError } = await supabase .from('placements') .insert({ application_id: data.application_id, recruiter_id: data.recruiter_id, fee_amount: data.fee_amount, status: 'pending' }) .select() .single(); if (placementError) throw placementError; // Update application stage const { error: appError } = await supabase .from('applications') .update({ stage: 'placed' }) .eq('id', data.application_id); if (appError) { // Rollback placement creation await supabase.from('placements').delete().eq('id', placement.id); throw appError; } return placement; }
See examples/transaction-patterns.ts for patterns.
6. Error Handling
async getById(id: string) { const { data, error } = await supabase .from('jobs') .select('*') .eq('id', id) .single(); if (error) { if (error.code === 'PGRST116') { throw new Error('Job not found'); } throw new Error(`Database error: ${error.message}`); } return data; }
Common Error Codes:
- No rows returned (not found)PGRST116
- Unique constraint violation23505
- Foreign key violation23503
See references/error-codes.md.
Migration Patterns
Creating Migrations
-- migrations/001_create_jobs_table.sql CREATE TABLE IF NOT EXISTS jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, company_id UUID NOT NULL REFERENCES companies(id), status VARCHAR(50) DEFAULT 'draft', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Indexes for common queries CREATE INDEX idx_jobs_company_id ON jobs(company_id); CREATE INDEX idx_jobs_status ON jobs(status); CREATE INDEX idx_jobs_created_at ON jobs(created_at DESC); -- Full-text search index CREATE INDEX idx_jobs_title_search ON jobs USING gin(to_tsvector('english', title));
See examples/migration-template.sql.
Migration Best Practices
- Always use IF NOT EXISTS
- Add indexes for foreign keys
- Add indexes for commonly filtered columns
- Use TIMESTAMPTZ for timestamps
- Set DEFAULT values appropriately
See references/migration-checklist.md.
Performance Optimization
Use Indexes Strategically
-- Foreign keys (always index) CREATE INDEX idx_applications_candidate_id ON applications(candidate_id); CREATE INDEX idx_applications_job_id ON applications(job_id); -- Commonly filtered columns CREATE INDEX idx_jobs_status ON jobs(status); CREATE INDEX idx_applications_stage ON applications(stage); -- Sorting columns (DESC for newest-first) CREATE INDEX idx_jobs_created_at ON jobs(created_at DESC); -- Composite indexes for common filter combinations CREATE INDEX idx_jobs_company_status ON jobs(company_id, status); -- Full-text search CREATE INDEX idx_jobs_title_fts ON jobs USING gin(to_tsvector('english', title));
See references/indexing-strategies.md.
Query Optimization
// ❌ WRONG - N+1 queries const jobs = await supabase.from('jobs').select('*'); for (const job of jobs) { const company = await supabase.from('companies').select('*').eq('id', job.company_id); } // ✅ CORRECT - Single query with JOIN const { data } = await supabase .from('jobs') .select(` *, company:companies(id, name, industry) `);
See examples/query-optimization.ts.
Type Safety
Generate TypeScript Types
// Use Supabase CLI to generate types // pnpm supabase gen types typescript --project-id einhgkqmxbkgdohwfayv > types/database.ts import { Database } from '@/types/database'; type Job = Database['public']['Tables']['jobs']['Row']; type JobInsert = Database['public']['Tables']['jobs']['Insert']; type JobUpdate = Database['public']['Tables']['jobs']['Update'];
See references/type-generation.md.
Common Patterns
Count Queries
async count(filters: JobFilters): Promise<number> { let query = supabase .from('jobs') .select('*', { count: 'exact', head: true }); if (filters.status) { query = query.eq('status', filters.status); } const { count } = await query; return count || 0; }
Upsert Pattern
// Insert or update if exists const { data } = await supabase .from('recruiters') .upsert({ user_id: userId, status: 'active', updated_at: new Date().toISOString() }, { onConflict: 'user_id' }) .select() .single();
Soft Delete Pattern
async delete(id: string) { const { data } = await supabase .from('jobs') .update({ deleted_at: new Date().toISOString(), status: 'deleted' }) .eq('id', id) .select() .single(); return data; } // Filter out soft-deleted records async list() { const { data } = await supabase .from('jobs') .select('*') .is('deleted_at', null); // Only non-deleted return data; }
See examples/common-patterns.ts.
Anti-Patterns to Avoid
❌ Direct Schema Queries Without Access Context
// WRONG - No role-based filtering async list() { return await supabase.from('candidates').select('*'); // Everyone sees all candidates! }
❌ N+1 Query Problem
// WRONG - Multiple queries in a loop const applications = await supabase.from('applications').select('*'); for (const app of applications) { const candidate = await supabase.from('candidates').select('*').eq('id', app.candidate_id); }
❌ Missing Indexes
-- WRONG - No index on foreign key CREATE TABLE applications ( candidate_id UUID REFERENCES candidates(id) -- Missing: CREATE INDEX idx_applications_candidate_id );
❌ Selecting Everything for Large Tables
// WRONG - Pulls all columns for thousands of rows const { data } = await supabase.from('applications').select('*');
References
- Repository with Access Context
- JOIN Patterns
- Query Optimization
- Migration Template
- Error Codes Reference
- Indexing Strategies
- Migration Checklist
Related Skills
- API layer patternsapi-specifications
- Access control patternsauthentication-authorization
- Advanced optimization techniquesperformance-optimization