aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
331 lines (272 loc) • 6.93 kB
Markdown
# Supabase Architecture Patterns
**Purpose:** Reference guide for Supabase implementation patterns
**Agent:** Dan (Data Engineer)
**Platform:** Supabase (PostgreSQL + Auth + Storage + Realtime)
---
## PROJECT STRUCTURE
### Recommended Organization
```
project/
├── supabase/
│ ├── migrations/ # Database migrations
│ │ ├── 00001_initial_schema.sql
│ │ └── 00002_add_rls_policies.sql
│ ├── functions/ # Edge Functions
│ │ └── function-name/
│ │ └── index.ts
│ ├── seed.sql # Seed data for development
│ └── config.toml # Supabase CLI config
├── src/
│ ├── lib/
│ │ └── supabase.ts # Supabase client initialization
│ └── types/
│ └── database.ts # Generated types from schema
└── .env.local # Environment variables
```
---
## CLIENT INITIALIZATION
### Browser Client (Public)
```typescript
import { createClient } from '@supabase/supabase-js'
import type { Database } from '@/types/database'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
```
### Server Client (Service Role)
```typescript
import { createClient } from '@supabase/supabase-js'
import type { Database } from '@/types/database'
// Only use server-side, never expose to client
export const supabaseAdmin = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)
```
---
## AUTHENTICATION PATTERNS
### Sign Up with Email
```typescript
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
full_name: 'User Name',
// Additional user metadata
}
}
})
```
### Sign In
```typescript
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
```
### OAuth Provider
```typescript
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
})
```
### Session Management
```typescript
// Get current session
const { data: { session } } = await supabase.auth.getSession()
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
// Handle sign in
} else if (event === 'SIGNED_OUT') {
// Handle sign out
}
})
```
---
## DATABASE PATTERNS
### Type-Safe Queries
```typescript
// Fetch with type safety
const { data, error } = await supabase
.from('profiles')
.select('id, username, avatar_url')
.eq('id', userId)
.single()
// Insert with return
const { data, error } = await supabase
.from('posts')
.insert({ title, content, author_id: userId })
.select()
.single()
// Update
const { data, error } = await supabase
.from('profiles')
.update({ username: newUsername })
.eq('id', userId)
.select()
.single()
// Delete
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
```
### Relationships
```typescript
// Fetch with foreign key relation
const { data, error } = await supabase
.from('posts')
.select(`
id,
title,
content,
author:profiles(id, username, avatar_url)
`)
.eq('id', postId)
.single()
```
### Pagination
```typescript
const pageSize = 10
const page = 1
const { data, count, error } = await supabase
.from('posts')
.select('*', { count: 'exact' })
.order('created_at', { ascending: false })
.range((page - 1) * pageSize, page * pageSize - 1)
```
---
## REALTIME PATTERNS
### Subscribe to Changes
```typescript
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `author_id=eq.${userId}`
},
(payload) => {
console.log('Change received:', payload)
}
)
.subscribe()
// Cleanup
channel.unsubscribe()
```
### Presence (Online Status)
```typescript
const channel = supabase.channel('room:lobby')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({ user_id: userId, online_at: new Date() })
}
})
```
---
## STORAGE PATTERNS
### Upload File
```typescript
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.png`, file, {
cacheControl: '3600',
upsert: true
})
```
### Get Public URL
```typescript
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`)
const publicUrl = data.publicUrl
```
### Signed URL (Private)
```typescript
const { data, error } = await supabase.storage
.from('private-docs')
.createSignedUrl(`${userId}/document.pdf`, 3600) // 1 hour
```
---
## EDGE FUNCTIONS
### Function Structure
```typescript
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! }
}
}
)
// Get user from JWT
const { data: { user } } = await supabaseClient.auth.getUser()
return new Response(
JSON.stringify({ message: `Hello ${user?.email}` }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
```
---
## TYPE GENERATION
### Generate Types from Schema
```bash
# Install Supabase CLI
npm install -g supabase
# Generate types
supabase gen types typescript --project-id your-project-id > src/types/database.ts
# Or from local
supabase gen types typescript --local > src/types/database.ts
```
### Usage
```typescript
import type { Database } from '@/types/database'
type Profile = Database['public']['Tables']['profiles']['Row']
type InsertProfile = Database['public']['Tables']['profiles']['Insert']
type UpdateProfile = Database['public']['Tables']['profiles']['Update']
```
---
## ERROR HANDLING
### Standard Pattern
```typescript
async function fetchProfile(userId: string) {
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', userId)
.single()
if (error) {
console.error('Error fetching profile:', error)
throw new Error(`Failed to fetch profile: ${error.message}`)
}
return data
}
```
---
**Reviewer:** ________ **Date:** ________
**Quality Gate:** [ ] PASS [ ] NEEDS REVIEW