Added Claude Skills

This commit is contained in:
2026-03-20 15:14:21 +08:00
parent a39e33bc6b
commit 27ebb89052
30 changed files with 7222 additions and 2 deletions
@@ -0,0 +1,384 @@
---
name: auth-integration
description: Authentication patterns and JWT handling for Supabase applications
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# Auth Integration Module
## Overview
Supabase Auth provides authentication with multiple providers, JWT-based sessions, and seamless integration with Row-Level Security policies.
## Client Setup
### Browser Client
```typescript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
```
### Server-Side Client (Next.js App Router)
```typescript
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from './database.types'
export function createServerSupabase() {
const cookieStore = cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name, value, options) {
cookieStore.set({ name, value, ...options })
},
remove(name, options) {
cookieStore.set({ name, value: '', ...options })
}
}
}
)
}
```
### Middleware Client (Next.js)
```typescript
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: { headers: request.headers }
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({ name, value, ...options })
response.cookies.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
request.cookies.set({ name, value: '', ...options })
response.cookies.set({ name, value: '', ...options })
}
}
}
)
await supabase.auth.getUser()
return response
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}
```
## Authentication Methods
### Email/Password Sign Up
```typescript
async function signUp(email: string, password: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
if (error) throw error
return data
}
```
### Email/Password Sign In
```typescript
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
return data
}
```
### OAuth Provider
```typescript
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent'
}
}
})
if (error) throw error
return data
}
```
### Magic Link
```typescript
async function signInWithMagicLink(email: string) {
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
if (error) throw error
return data
}
```
## Session Management
### Get Current User
```typescript
async function getCurrentUser() {
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
return user
}
```
### Get Session
```typescript
async function getSession() {
const { data: { session }, error } = await supabase.auth.getSession()
if (error) throw error
return session
}
```
### Sign Out
```typescript
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) throw error
}
```
### Listen to Auth Changes
```typescript
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session?.user)
}
if (event === 'SIGNED_OUT') {
console.log('User signed out')
}
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed')
}
}
)
return () => subscription.unsubscribe()
}, [])
```
## Auth Callback Handler
### Next.js App Router
```typescript
// app/auth/callback/route.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { NextResponse, type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/'
if (code) {
const cookieStore = cookies()
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name, value, options) {
cookieStore.set({ name, value, ...options })
},
remove(name, options) {
cookieStore.set({ name, value: '', ...options })
}
}
}
)
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(`${origin}${next}`)
}
}
return NextResponse.redirect(`${origin}/auth/error`)
}
```
## Protected Routes
### Server Component Protection
```typescript
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createServerSupabase } from '@/lib/supabase/server'
export default async function DashboardPage() {
const supabase = createServerSupabase()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
return <Dashboard user={user} />
}
```
### Client Component Protection
```typescript
'use client'
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { supabase } from '@/lib/supabase/client'
export function useRequireAuth() {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
useEffect(() => {
supabase.auth.getUser().then(({ data: { user } }) => {
if (!user) {
router.push('/login')
} else {
setUser(user)
}
setLoading(false)
})
}, [router])
return { user, loading }
}
```
## Custom Claims
### Setting Custom Claims (Edge Function)
```typescript
// supabase/functions/set-claims/index.ts
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
const { userId, claims } = await req.json()
// Update user metadata (available in JWT)
const { error } = await supabase.auth.admin.updateUserById(userId, {
app_metadata: claims
})
if (error) throw error
return new Response(JSON.stringify({ success: true }))
})
```
### Reading Claims in RLS
```sql
-- Access claims in RLS policies
CREATE POLICY "admin_only" ON admin_data FOR ALL
USING ((auth.jwt() ->> 'role')::text = 'admin');
```
## Password Reset
```typescript
async function resetPassword(email: string) {
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/auth/reset-password`
})
if (error) throw error
return data
}
async function updatePassword(newPassword: string) {
const { data, error } = await supabase.auth.updateUser({
password: newPassword
})
if (error) throw error
return data
}
```
## Context7 Query Examples
For latest Auth documentation:
Topic: "supabase auth signIn signUp"
Topic: "supabase ssr server client"
Topic: "auth jwt claims custom"
---
Related Modules:
- row-level-security.md - Auth integration with RLS
- typescript-patterns.md - Type-safe auth patterns
- edge-functions.md - Server-side auth verification
@@ -0,0 +1,371 @@
---
name: edge-functions
description: Serverless Deno functions at the edge with authentication and rate limiting
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# Edge Functions Module
## Overview
Supabase Edge Functions are serverless functions running on the Deno runtime at the edge, providing low-latency responses globally.
## Basic Edge Function
### Function Structure
```typescript
// supabase/functions/api/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'
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type'
}
serve(async (req) => {
// Handle CORS preflight
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
try {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Process request
const body = await req.json()
return new Response(
JSON.stringify({ success: true, data: body }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
})
```
## Authentication
### JWT Token Verification
```typescript
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Verify JWT token
const authHeader = req.headers.get('authorization')
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
const { data: { user }, error } = await supabase.auth.getUser(
authHeader.replace('Bearer ', '')
)
if (error || !user) {
return new Response(
JSON.stringify({ error: 'Invalid token' }),
{ status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
// User is authenticated, proceed with request
return new Response(
JSON.stringify({ success: true, user_id: user.id }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
```
### Using User Context Client
Create a client that inherits user permissions:
```typescript
serve(async (req) => {
const authHeader = req.headers.get('authorization')!
// Client with user's permissions (respects RLS)
const supabaseUser = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
// This query respects RLS policies
const { data, error } = await supabaseUser
.from('projects')
.select('*')
return new Response(JSON.stringify({ data }))
})
```
## Rate Limiting
### Database-Based Rate Limiting
```sql
-- Rate limits table
CREATE TABLE rate_limits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
identifier TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_rate_limits_lookup ON rate_limits(identifier, created_at);
```
### Rate Limit Function
```typescript
async function checkRateLimit(
supabase: SupabaseClient,
identifier: string,
limit: number,
windowSeconds: number
): Promise<boolean> {
const windowStart = new Date(Date.now() - windowSeconds * 1000).toISOString()
const { count } = await supabase
.from('rate_limits')
.select('*', { count: 'exact', head: true })
.eq('identifier', identifier)
.gte('created_at', windowStart)
if (count && count >= limit) {
return false
}
await supabase.from('rate_limits').insert({ identifier })
return true
}
```
### Usage in Edge Function
```typescript
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Get client identifier (IP or user ID)
const identifier = req.headers.get('x-forwarded-for') || 'anonymous'
// 100 requests per minute
const allowed = await checkRateLimit(supabase, identifier, 100, 60)
if (!allowed) {
return new Response(
JSON.stringify({ error: 'Rate limit exceeded' }),
{ status: 429, headers: corsHeaders }
)
}
// Process request...
})
```
## External API Integration
### Webhook Handler
```typescript
serve(async (req) => {
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Verify webhook signature
const signature = req.headers.get('x-webhook-signature')
const body = await req.text()
const expectedSignature = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(body + Deno.env.get('WEBHOOK_SECRET'))
)
if (!verifySignature(signature, expectedSignature)) {
return new Response('Invalid signature', { status: 401 })
}
const payload = JSON.parse(body)
// Process webhook
await supabase.from('webhook_events').insert({
type: payload.type,
data: payload.data,
processed: false
})
return new Response('OK', { status: 200 })
})
```
### External API Call
```typescript
serve(async (req) => {
const { query } = await req.json()
// Call external API
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'text-embedding-ada-002',
input: query
})
})
const data = await response.json()
return new Response(
JSON.stringify({ embedding: data.data[0].embedding }),
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
})
```
## Error Handling
### Structured Error Response
```typescript
interface ErrorResponse {
error: string
code: string
details?: unknown
}
function errorResponse(
message: string,
code: string,
status: number,
details?: unknown
): Response {
const body: ErrorResponse = { error: message, code, details }
return new Response(
JSON.stringify(body),
{ status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
)
}
serve(async (req) => {
try {
// ... processing
} catch (error) {
if (error instanceof AuthError) {
return errorResponse('Authentication failed', 'AUTH_ERROR', 401)
}
if (error instanceof ValidationError) {
return errorResponse('Invalid input', 'VALIDATION_ERROR', 400, error.details)
}
console.error('Unexpected error:', error)
return errorResponse('Internal server error', 'INTERNAL_ERROR', 500)
}
})
```
## Deployment
### Local Development
```bash
supabase functions serve api --env-file .env.local
```
### Deploy Function
```bash
supabase functions deploy api
```
### Set Secrets
```bash
supabase secrets set OPENAI_API_KEY=sk-xxx
supabase secrets set WEBHOOK_SECRET=whsec-xxx
```
### List Functions
```bash
supabase functions list
```
## Best Practices
### Cold Start Optimization
Keep imports minimal at the top level:
```typescript
// Good: Import only what's needed
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
// Bad: Heavy imports at top level increase cold start
// import { everything } from 'large-library'
```
### Response Streaming
Stream large responses:
```typescript
serve(async (req) => {
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 10; i++) {
await new Promise(r => setTimeout(r, 100))
controller.enqueue(new TextEncoder().encode(`data: ${i}\n\n`))
}
controller.close()
}
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' }
})
})
```
## Context7 Query Examples
For latest Edge Functions documentation:
Topic: "edge functions deno runtime"
Topic: "supabase functions deploy secrets"
Topic: "edge functions cors authentication"
---
Related Modules:
- auth-integration.md - Authentication patterns
- typescript-patterns.md - Client invocation
@@ -0,0 +1,231 @@
---
name: postgresql-pgvector
description: PostgreSQL 16 with pgvector extension for AI embeddings and semantic search
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# PostgreSQL 16 + pgvector Module
## Overview
PostgreSQL 16 with pgvector extension enables AI-powered semantic search through vector embeddings storage and similarity search operations.
## Extension Setup
Enable required extensions for vector operations:
```sql
-- Enable required extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS vector;
```
## Embeddings Table Schema
Create a table optimized for storing AI embeddings:
```sql
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding vector(1536), -- OpenAI ada-002 dimensions
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### Common Embedding Dimensions
- OpenAI ada-002: 1536 dimensions
- OpenAI text-embedding-3-small: 1536 dimensions
- OpenAI text-embedding-3-large: 3072 dimensions
- Cohere embed-english-v3.0: 1024 dimensions
- Google PaLM: 768 dimensions
## Index Strategies
### HNSW Index (Recommended)
HNSW (Hierarchical Navigable Small World) provides fast approximate nearest neighbor search:
```sql
CREATE INDEX idx_documents_embedding ON documents
USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
```
Parameters:
- m: Maximum number of connections per layer (default 16, higher = more accurate but slower)
- ef_construction: Size of dynamic candidate list during construction (default 64)
### IVFFlat Index (Large Datasets)
IVFFlat is better for datasets with millions of rows:
```sql
CREATE INDEX idx_documents_ivf ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
```
Guidelines for lists parameter:
- Less than 1M rows: lists = rows / 1000
- More than 1M rows: lists = sqrt(rows)
## Distance Operations
Available distance operators:
- `<->` - Euclidean distance (L2)
- `<#>` - Negative inner product
- `<=>` - Cosine distance
For normalized embeddings, cosine distance is recommended.
## Semantic Search Function
Basic semantic search with threshold and limit:
```sql
CREATE OR REPLACE FUNCTION search_documents(
query_embedding vector(1536),
match_threshold FLOAT DEFAULT 0.8,
match_count INT DEFAULT 10
) RETURNS TABLE (id UUID, content TEXT, similarity FLOAT)
LANGUAGE plpgsql AS $$
BEGIN
RETURN QUERY SELECT d.id, d.content,
1 - (d.embedding <=> query_embedding) AS similarity
FROM documents d
WHERE 1 - (d.embedding <=> query_embedding) > match_threshold
ORDER BY d.embedding <=> query_embedding
LIMIT match_count;
END; $$;
```
### Usage Example
```sql
SELECT * FROM search_documents(
'[0.1, 0.2, ...]'::vector(1536),
0.75,
20
);
```
## Hybrid Search
Combine vector similarity with full-text search for better results:
```sql
CREATE OR REPLACE FUNCTION hybrid_search(
query_text TEXT,
query_embedding vector(1536),
match_count INT DEFAULT 10,
full_text_weight FLOAT DEFAULT 0.3,
semantic_weight FLOAT DEFAULT 0.7
) RETURNS TABLE (id UUID, content TEXT, score FLOAT) AS $$
BEGIN
RETURN QUERY
WITH semantic AS (
SELECT e.id, e.content, 1 - (e.embedding <=> query_embedding) AS similarity
FROM documents e ORDER BY e.embedding <=> query_embedding LIMIT match_count * 2
),
full_text AS (
SELECT e.id, e.content,
ts_rank(to_tsvector('english', e.content), plainto_tsquery('english', query_text)) AS rank
FROM documents e
WHERE to_tsvector('english', e.content) @@ plainto_tsquery('english', query_text)
LIMIT match_count * 2
)
SELECT COALESCE(s.id, f.id), COALESCE(s.content, f.content),
(COALESCE(s.similarity, 0) * semantic_weight + COALESCE(f.rank, 0) * full_text_weight)
FROM semantic s FULL OUTER JOIN full_text f ON s.id = f.id
ORDER BY 3 DESC LIMIT match_count;
END; $$ LANGUAGE plpgsql;
```
## Full-Text Search Index
Add GIN index for efficient full-text search:
```sql
CREATE INDEX idx_documents_content_fts ON documents
USING gin(to_tsvector('english', content));
```
## Performance Optimization
### Query Performance
Set appropriate ef_search for HNSW queries:
```sql
SET hnsw.ef_search = 100; -- Higher = more accurate, slower
```
### Batch Insertions
Use COPY or multi-row INSERT for bulk embeddings:
```sql
INSERT INTO documents (content, embedding, metadata)
VALUES
('Content 1', '[...]'::vector(1536), '{"source": "doc1"}'),
('Content 2', '[...]'::vector(1536), '{"source": "doc2"}'),
('Content 3', '[...]'::vector(1536), '{"source": "doc3"}');
```
### Index Maintenance
Reindex after large bulk insertions:
```sql
REINDEX INDEX CONCURRENTLY idx_documents_embedding;
```
## Metadata Filtering
Combine vector search with JSONB metadata filters:
```sql
CREATE OR REPLACE FUNCTION search_with_filters(
query_embedding vector(1536),
filter_metadata JSONB,
match_count INT DEFAULT 10
) RETURNS TABLE (id UUID, content TEXT, similarity FLOAT) AS $$
BEGIN
RETURN QUERY SELECT d.id, d.content,
1 - (d.embedding <=> query_embedding) AS similarity
FROM documents d
WHERE d.metadata @> filter_metadata
ORDER BY d.embedding <=> query_embedding
LIMIT match_count;
END; $$;
```
### Usage with Filters
```sql
SELECT * FROM search_with_filters(
'[0.1, 0.2, ...]'::vector(1536),
'{"category": "technical", "language": "en"}'::jsonb,
10
);
```
## Context7 Query Examples
For latest pgvector documentation:
Topic: "pgvector extension indexes hnsw ivfflat"
Topic: "vector similarity search operators"
Topic: "postgresql full-text search tsvector"
---
Related Modules:
- row-level-security.md - Secure vector data access
- typescript-patterns.md - Client-side search implementation
@@ -0,0 +1,354 @@
---
name: realtime-presence
description: Real-time subscriptions and presence tracking for collaborative features
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# Real-time and Presence Module
## Overview
Supabase provides real-time capabilities through Postgres Changes (database change notifications) and Presence (user state tracking) for building collaborative applications.
## Postgres Changes Subscription
### Basic Setup
Subscribe to all changes on a table:
```typescript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
const channel = supabase.channel('db-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'messages' },
(payload) => console.log('Change:', payload)
)
.subscribe()
```
### Event Types
Available events:
- `INSERT` - New row added
- `UPDATE` - Row modified
- `DELETE` - Row removed
- `*` - All events
### Filtered Subscriptions
Filter changes by specific conditions:
```typescript
supabase.channel('project-updates')
.on('postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'projects',
filter: `id=eq.${projectId}`
},
(payload) => handleProjectUpdate(payload.new)
)
.subscribe()
```
### Multiple Tables
Subscribe to multiple tables on one channel:
```typescript
const channel = supabase.channel('app-changes')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'tasks' },
handleTaskChange
)
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'comments' },
handleCommentChange
)
.subscribe()
```
## Presence Tracking
### Presence State Interface
```typescript
interface PresenceState {
user_id: string
online_at: string
typing?: boolean
cursor?: { x: number; y: number }
}
```
### Channel Setup with Presence
```typescript
const channel = supabase.channel('room:collaborative-doc', {
config: { presence: { key: userId } }
})
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<PresenceState>()
console.log('Online users:', Object.keys(state))
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key, newPresences)
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key, leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString()
})
}
})
```
### Update Presence State
Update user presence in real-time:
```typescript
// Track typing status
await channel.track({ typing: true })
// Track cursor position
await channel.track({ cursor: { x: 100, y: 200 } })
// Clear typing after timeout
setTimeout(async () => {
await channel.track({ typing: false })
}, 1000)
```
## Collaborative Features
### Collaborative Cursors
```typescript
interface CursorState {
user_id: string
user_name: string
cursor: { x: number; y: number }
color: string
}
function setupCollaborativeCursors(documentId: string, userId: string, userName: string) {
const channel = supabase.channel(`cursors:${documentId}`, {
config: { presence: { key: userId } }
})
const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7']
const userColor = colors[Math.abs(userId.hashCode()) % colors.length]
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<CursorState>()
renderCursors(Object.values(state).flat())
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
user_name: userName,
cursor: { x: 0, y: 0 },
color: userColor
})
}
})
// Track mouse movement
document.addEventListener('mousemove', async (e) => {
await channel.track({
user_id: userId,
user_name: userName,
cursor: { x: e.clientX, y: e.clientY },
color: userColor
})
})
return channel
}
```
### Live Editing Indicators
```typescript
interface EditingState {
user_id: string
user_name: string
editing_field: string | null
}
function setupFieldLocking(formId: string) {
const channel = supabase.channel(`form:${formId}`, {
config: { presence: { key: currentUserId } }
})
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<EditingState>()
updateFieldLocks(Object.values(state).flat())
})
.subscribe()
return {
startEditing: async (fieldName: string) => {
await channel.track({
user_id: currentUserId,
user_name: currentUserName,
editing_field: fieldName
})
},
stopEditing: async () => {
await channel.track({
user_id: currentUserId,
user_name: currentUserName,
editing_field: null
})
}
}
}
```
## Broadcast Messages
Send arbitrary messages to channel subscribers:
```typescript
const channel = supabase.channel('room:chat')
// Subscribe to broadcasts
channel
.on('broadcast', { event: 'message' }, ({ payload }) => {
console.log('Received:', payload)
})
.subscribe()
// Send broadcast
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello everyone!', sender: userId }
})
```
## Subscription Management
### Unsubscribe
```typescript
// Unsubscribe from specific channel
await supabase.removeChannel(channel)
// Unsubscribe from all channels
await supabase.removeAllChannels()
```
### Subscription Status
```typescript
channel.subscribe((status) => {
switch (status) {
case 'SUBSCRIBED':
console.log('Connected to channel')
break
case 'CLOSED':
console.log('Channel closed')
break
case 'CHANNEL_ERROR':
console.log('Channel error')
break
case 'TIMED_OUT':
console.log('Connection timed out')
break
}
})
```
## React Integration
### Custom Hook for Presence
```typescript
import { useEffect, useState } from 'react'
import { supabase } from './supabase/client'
export function usePresence<T>(channelName: string, userId: string, initialState: T) {
const [presences, setPresences] = useState<Record<string, T[]>>({})
useEffect(() => {
const channel = supabase.channel(channelName, {
config: { presence: { key: userId } }
})
channel
.on('presence', { event: 'sync' }, () => {
setPresences(channel.presenceState<T>())
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track(initialState)
}
})
return () => {
supabase.removeChannel(channel)
}
}, [channelName, userId])
const updatePresence = async (state: Partial<T>) => {
const channel = supabase.getChannels().find(c => c.topic === channelName)
if (channel) {
await channel.track({ ...initialState, ...state } as T)
}
}
return { presences, updatePresence }
}
```
### Usage
```typescript
function CollaborativeEditor({ documentId, userId }) {
const { presences, updatePresence } = usePresence(
`doc:${documentId}`,
userId,
{ user_id: userId, typing: false, cursor: null }
)
return (
<div>
{Object.values(presences).flat().map(p => (
<Cursor key={p.user_id} position={p.cursor} />
))}
</div>
)
}
```
## Context7 Query Examples
For latest real-time documentation:
Topic: "realtime postgres_changes subscription"
Topic: "presence tracking channel"
Topic: "broadcast messages supabase"
---
Related Modules:
- typescript-patterns.md - Client architecture
- auth-integration.md - Authenticated subscriptions
@@ -0,0 +1,286 @@
---
name: row-level-security
description: RLS policies for multi-tenant data isolation and access control
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# Row-Level Security (RLS) Module
## Overview
Row-Level Security provides automatic data isolation at the database level, ensuring users can only access data they are authorized to see.
## Basic Setup
Enable RLS on a table:
```sql
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
```
## Policy Types
RLS policies can be created for specific operations:
- SELECT: Controls read access
- INSERT: Controls creation
- UPDATE: Controls modification
- DELETE: Controls removal
- ALL: Applies to all operations
## Basic Tenant Isolation
### JWT-Based Tenant Isolation
Extract tenant ID from JWT claims:
```sql
CREATE POLICY "tenant_isolation" ON projects FOR ALL
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::UUID);
```
### Owner-Based Access
Restrict access to resource owners:
```sql
CREATE POLICY "owner_access" ON projects FOR ALL
USING (owner_id = auth.uid());
```
## Hierarchical Access Patterns
### Organization Membership
Allow access based on organization membership:
```sql
CREATE POLICY "org_member_select" ON organizations FOR SELECT
USING (id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
```
### Role-Based Modification
Restrict modifications to specific roles:
```sql
CREATE POLICY "org_admin_modify" ON organizations FOR UPDATE
USING (id IN (
SELECT org_id FROM org_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
));
```
### Cascading Project Access
Grant project access through organization membership:
```sql
CREATE POLICY "project_access" ON projects FOR ALL
USING (org_id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
```
## Service Role Bypass
Allow service role to bypass RLS for server-side operations:
```sql
CREATE POLICY "service_bypass" ON organizations FOR ALL TO service_role USING (true);
```
## Multi-Tenant SaaS Schema
### Complete Schema Setup
```sql
-- Organizations (tenants)
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL,
slug TEXT UNIQUE NOT NULL,
plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Organization members with roles
CREATE TABLE organization_members (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
joined_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(organization_id, user_id)
);
-- Projects within organizations
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
owner_id UUID NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
```
### Enable RLS on All Tables
```sql
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
```
### Comprehensive RLS Policies
```sql
-- Organization read access
CREATE POLICY "org_member_select" ON organizations FOR SELECT
USING (id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
-- Organization admin update
CREATE POLICY "org_admin_update" ON organizations FOR UPDATE
USING (id IN (SELECT organization_id FROM organization_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
-- Project member access
CREATE POLICY "project_member_access" ON projects FOR ALL
USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
-- Member management (admin only)
CREATE POLICY "member_admin_manage" ON organization_members FOR ALL
USING (organization_id IN (SELECT organization_id FROM organization_members
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
```
## Helper Functions
### Check Organization Membership
```sql
CREATE OR REPLACE FUNCTION is_org_member(org_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM organization_members
WHERE organization_id = org_id AND user_id = auth.uid()
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
### Check Organization Role
```sql
CREATE OR REPLACE FUNCTION has_org_role(org_id UUID, required_roles TEXT[])
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM organization_members
WHERE organization_id = org_id
AND user_id = auth.uid()
AND role = ANY(required_roles)
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
### Usage in Policies
```sql
CREATE POLICY "project_admin_delete" ON projects FOR DELETE
USING (has_org_role(organization_id, ARRAY['owner', 'admin']));
```
## Performance Optimization
### Index for RLS Queries
Create indexes on foreign keys used in RLS policies:
```sql
CREATE INDEX idx_org_members_user ON organization_members(user_id);
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
CREATE INDEX idx_projects_org ON projects(organization_id);
```
### Materialized View for Complex Policies
For complex permission checks, use materialized views:
```sql
CREATE MATERIALIZED VIEW user_accessible_projects AS
SELECT p.id as project_id, om.user_id, om.role
FROM projects p
JOIN organization_members om ON p.organization_id = om.organization_id;
CREATE INDEX idx_uap_user ON user_accessible_projects(user_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY user_accessible_projects;
```
## Testing RLS Policies
### Test as Authenticated User
```sql
SET request.jwt.claim.sub = 'user-uuid-here';
SET request.jwt.claims = '{"role": "authenticated"}';
SELECT * FROM projects; -- Returns only accessible projects
```
### Verify Policy Restrictions
```sql
-- Should fail if not a member
INSERT INTO projects (organization_id, name, owner_id)
VALUES ('non-member-org-id', 'Test', auth.uid());
```
## Common Patterns
### Public Read, Owner Write
```sql
CREATE POLICY "public_read" ON posts FOR SELECT USING (true);
CREATE POLICY "owner_write" ON posts FOR INSERT WITH CHECK (author_id = auth.uid());
CREATE POLICY "owner_update" ON posts FOR UPDATE USING (author_id = auth.uid());
CREATE POLICY "owner_delete" ON posts FOR DELETE USING (author_id = auth.uid());
```
### Draft vs Published
```sql
CREATE POLICY "published_read" ON articles FOR SELECT
USING (status = 'published' OR author_id = auth.uid());
```
### Time-Based Access
```sql
CREATE POLICY "active_subscription" ON premium_content FOR SELECT
USING (
EXISTS (
SELECT 1 FROM subscriptions
WHERE user_id = auth.uid()
AND expires_at > NOW()
)
);
```
## Context7 Query Examples
For latest RLS documentation:
Topic: "row level security policies supabase"
Topic: "auth.uid auth.jwt functions"
Topic: "rls performance optimization"
---
Related Modules:
- auth-integration.md - Authentication patterns
- typescript-patterns.md - Client-side access patterns
@@ -0,0 +1,319 @@
---
name: storage-cdn
description: File storage with image transformations and CDN delivery
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# Storage and CDN Module
## Overview
Supabase Storage provides file storage with automatic image transformations, CDN delivery, and fine-grained access control through storage policies.
## Basic Upload
### Upload File
```typescript
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
async function uploadFile(file: File, bucket: string, path: string) {
const { data, error } = await supabase.storage
.from(bucket)
.upload(path, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
return data.path
}
```
### Upload with User Context
```typescript
async function uploadUserFile(file: File, userId: string) {
const fileName = `${userId}/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('user-files')
.upload(fileName, file, {
cacheControl: '3600',
upsert: false
})
if (error) throw error
return data
}
```
## Image Transformations
### Get Transformed URL
```typescript
async function uploadImage(file: File, userId: string) {
const fileName = `${userId}/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('images')
.upload(fileName, file, { cacheControl: '3600', upsert: false })
if (error) throw error
// Get original URL
const { data: { publicUrl } } = supabase.storage
.from('images')
.getPublicUrl(fileName)
// Get resized URL
const { data: { publicUrl: resizedUrl } } = supabase.storage
.from('images')
.getPublicUrl(fileName, {
transform: { width: 800, height: 600, resize: 'contain' }
})
// Get thumbnail URL
const { data: { publicUrl: thumbnailUrl } } = supabase.storage
.from('images')
.getPublicUrl(fileName, {
transform: { width: 200, height: 200, resize: 'cover' }
})
return { originalPath: data.path, publicUrl, resizedUrl, thumbnailUrl }
}
```
### Transform Options
Available transformation parameters:
- width: Target width in pixels
- height: Target height in pixels
- resize: 'cover' | 'contain' | 'fill'
- format: 'origin' | 'avif' | 'webp'
- quality: 1-100
### Example Transforms
```typescript
// Square thumbnail with crop
const thumbnail = supabase.storage
.from('images')
.getPublicUrl(path, {
transform: { width: 150, height: 150, resize: 'cover' }
})
// WebP format for smaller size
const webp = supabase.storage
.from('images')
.getPublicUrl(path, {
transform: { width: 800, format: 'webp', quality: 80 }
})
// Responsive image
const responsive = supabase.storage
.from('images')
.getPublicUrl(path, {
transform: { width: 400, resize: 'contain' }
})
```
## Bucket Management
### Create Bucket
```sql
-- Via SQL
INSERT INTO storage.buckets (id, name, public)
VALUES ('images', 'images', true);
```
### Bucket Policies
```sql
-- Allow authenticated users to upload to their folder
CREATE POLICY "User upload" ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'user-files' AND (storage.foldername(name))[1] = auth.uid()::text);
-- Allow public read on images bucket
CREATE POLICY "Public read" ON storage.objects
FOR SELECT
TO public
USING (bucket_id = 'images');
-- Allow users to delete their own files
CREATE POLICY "User delete" ON storage.objects
FOR DELETE
TO authenticated
USING (bucket_id = 'user-files' AND (storage.foldername(name))[1] = auth.uid()::text);
```
## Download Files
### Get Signed URL
For private buckets:
```typescript
async function getSignedUrl(bucket: string, path: string, expiresIn: number = 3600) {
const { data, error } = await supabase.storage
.from(bucket)
.createSignedUrl(path, expiresIn)
if (error) throw error
return data.signedUrl
}
```
### Download File
```typescript
async function downloadFile(bucket: string, path: string) {
const { data, error } = await supabase.storage
.from(bucket)
.download(path)
if (error) throw error
return data // Blob
}
```
## File Management
### List Files
```typescript
async function listFiles(bucket: string, folder: string) {
const { data, error } = await supabase.storage
.from(bucket)
.list(folder, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
if (error) throw error
return data
}
```
### Delete File
```typescript
async function deleteFile(bucket: string, paths: string[]) {
const { data, error } = await supabase.storage
.from(bucket)
.remove(paths)
if (error) throw error
return data
}
```
### Move/Rename File
```typescript
async function moveFile(bucket: string, fromPath: string, toPath: string) {
const { data, error } = await supabase.storage
.from(bucket)
.move(fromPath, toPath)
if (error) throw error
return data
}
```
## React Integration
### Upload Component
```typescript
function FileUploader({ bucket, onUpload }: Props) {
const [uploading, setUploading] = useState(false)
async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0]
if (!file) return
setUploading(true)
try {
const path = `${Date.now()}-${file.name}`
const { error } = await supabase.storage
.from(bucket)
.upload(path, file)
if (error) throw error
onUpload(path)
} finally {
setUploading(false)
}
}
return (
<input
type="file"
onChange={handleUpload}
disabled={uploading}
/>
)
}
```
### Image with Fallback
```typescript
function StorageImage({ path, bucket, width, height, fallback }: Props) {
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(path, {
transform: { width, height, resize: 'cover' }
})
return (
<img
src={publicUrl}
alt=""
onError={(e) => { e.currentTarget.src = fallback }}
width={width}
height={height}
/>
)
}
```
## Best Practices
File Organization:
- Use user ID as folder prefix for user content
- Include timestamp in filenames to prevent collisions
- Use consistent naming conventions
Performance:
- Set appropriate cache-control headers
- Use image transformations instead of storing multiple sizes
- Leverage CDN for global delivery
Security:
- Always use RLS-style policies for storage
- Use signed URLs for private content
- Validate file types before upload
## Context7 Query Examples
For latest Storage documentation:
Topic: "supabase storage upload download"
Topic: "storage image transformations"
Topic: "storage bucket policies"
---
Related Modules:
- row-level-security.md - Storage access policies
- typescript-patterns.md - Client patterns
@@ -0,0 +1,453 @@
---
name: typescript-patterns
description: TypeScript client patterns and service layer architecture for Supabase
parent-skill: moai-platform-supabase
version: 1.0.0
updated: 2026-01-06
---
# TypeScript Patterns Module
## Overview
Type-safe Supabase client patterns for building maintainable full-stack applications with TypeScript.
## Type Generation
### Generate Types from Database
```bash
supabase gen types typescript --project-id your-project-id > database.types.ts
```
### Database Types Structure
```typescript
// database.types.ts
export type Database = {
public: {
Tables: {
projects: {
Row: {
id: string
name: string
organization_id: string
owner_id: string
created_at: string
}
Insert: {
id?: string
name: string
organization_id: string
owner_id: string
created_at?: string
}
Update: {
id?: string
name?: string
organization_id?: string
owner_id?: string
created_at?: string
}
}
// ... other tables
}
Functions: {
search_documents: {
Args: {
query_embedding: number[]
match_threshold: number
match_count: number
}
Returns: { id: string; content: string; similarity: number }[]
}
}
}
}
```
## Client Configuration
### Browser Client with Types
```typescript
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
```
### Server Client (Next.js App Router)
```typescript
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from './database.types'
export function createServerSupabase() {
const cookieStore = cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name, value, options) {
cookieStore.set({ name, value, ...options })
},
remove(name, options) {
cookieStore.set({ name, value: '', ...options })
}
}
}
)
}
```
## Service Layer Pattern
### Base Service
```typescript
import { supabase } from './supabase/client'
import { Database } from './database.types'
type Tables = Database['public']['Tables']
export abstract class BaseService<T extends keyof Tables> {
constructor(protected tableName: T) {}
async findAll() {
const { data, error } = await supabase
.from(this.tableName)
.select('*')
if (error) throw error
return data as Tables[T]['Row'][]
}
async findById(id: string) {
const { data, error } = await supabase
.from(this.tableName)
.select('*')
.eq('id', id)
.single()
if (error) throw error
return data as Tables[T]['Row']
}
async create(item: Tables[T]['Insert']) {
const { data, error } = await supabase
.from(this.tableName)
.insert(item)
.select()
.single()
if (error) throw error
return data as Tables[T]['Row']
}
async update(id: string, item: Tables[T]['Update']) {
const { data, error } = await supabase
.from(this.tableName)
.update(item)
.eq('id', id)
.select()
.single()
if (error) throw error
return data as Tables[T]['Row']
}
async delete(id: string) {
const { error } = await supabase
.from(this.tableName)
.delete()
.eq('id', id)
if (error) throw error
}
}
```
### Document Service with Embeddings
```typescript
import { supabase } from './supabase/client'
export class DocumentService {
async create(projectId: string, title: string, content: string) {
const { data: { user } } = await supabase.auth.getUser()
const { data, error } = await supabase
.from('documents')
.insert({
project_id: projectId,
title,
content,
created_by: user!.id
})
.select()
.single()
if (error) throw error
// Generate embedding asynchronously
await supabase.functions.invoke('generate-embedding', {
body: { documentId: data.id, content }
})
return data
}
async semanticSearch(projectId: string, query: string) {
// Get embedding for query
const { data: embeddingData } = await supabase.functions.invoke(
'get-embedding',
{ body: { text: query } }
)
// Search using RPC
const { data, error } = await supabase.rpc('search_documents', {
p_project_id: projectId,
p_query_embedding: embeddingData.embedding,
p_match_threshold: 0.7,
p_match_count: 10
})
if (error) throw error
return data
}
async findByProject(projectId: string) {
const { data, error } = await supabase
.from('documents')
.select('*, created_by_user:profiles!created_by(*)')
.eq('project_id', projectId)
.order('created_at', { ascending: false })
if (error) throw error
return data
}
subscribeToChanges(projectId: string, callback: (payload: any) => void) {
return supabase.channel(`documents:${projectId}`)
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: 'documents',
filter: `project_id=eq.${projectId}`
},
callback
)
.subscribe()
}
}
export const documentService = new DocumentService()
```
## React Query Integration
### Query Keys
```typescript
export const queryKeys = {
projects: {
all: ['projects'] as const,
list: (filters?: ProjectFilters) => [...queryKeys.projects.all, 'list', filters] as const,
detail: (id: string) => [...queryKeys.projects.all, 'detail', id] as const
},
documents: {
all: ['documents'] as const,
list: (projectId: string) => [...queryKeys.documents.all, 'list', projectId] as const,
search: (projectId: string, query: string) =>
[...queryKeys.documents.all, 'search', projectId, query] as const
}
}
```
### Custom Hooks
```typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { documentService } from '@/services/document-service'
export function useDocuments(projectId: string) {
return useQuery({
queryKey: queryKeys.documents.list(projectId),
queryFn: () => documentService.findByProject(projectId)
})
}
export function useSemanticSearch(projectId: string, query: string) {
return useQuery({
queryKey: queryKeys.documents.search(projectId, query),
queryFn: () => documentService.semanticSearch(projectId, query),
enabled: query.length > 2
})
}
export function useCreateDocument(projectId: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ title, content }: { title: string; content: string }) =>
documentService.create(projectId, title, content),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.documents.list(projectId)
})
}
})
}
```
## Real-time with React
### Subscription Hook
```typescript
import { useEffect } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { supabase } from '@/lib/supabase/client'
export function useRealtimeDocuments(projectId: string) {
const queryClient = useQueryClient()
useEffect(() => {
const channel = supabase
.channel(`documents:${projectId}`)
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: 'documents',
filter: `project_id=eq.${projectId}`
},
() => {
queryClient.invalidateQueries({
queryKey: queryKeys.documents.list(projectId)
})
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [projectId, queryClient])
}
```
### Optimistic Updates
```typescript
export function useUpdateDocument() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, updates }: { id: string; updates: Partial<Document> }) => {
const { data, error } = await supabase
.from('documents')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
return data
},
onMutate: async ({ id, updates }) => {
await queryClient.cancelQueries({ queryKey: ['documents'] })
const previousDocuments = queryClient.getQueryData(['documents'])
queryClient.setQueryData(['documents'], (old: Document[]) =>
old.map(doc => doc.id === id ? { ...doc, ...updates } : doc)
)
return { previousDocuments }
},
onError: (err, variables, context) => {
if (context?.previousDocuments) {
queryClient.setQueryData(['documents'], context.previousDocuments)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['documents'] })
}
})
}
```
## Error Handling
### Custom Error Types
```typescript
export class SupabaseError extends Error {
constructor(
message: string,
public code: string,
public details?: unknown
) {
super(message)
this.name = 'SupabaseError'
}
}
export function handleSupabaseError(error: PostgrestError): never {
switch (error.code) {
case '23505':
throw new SupabaseError('Resource already exists', 'DUPLICATE', error)
case '23503':
throw new SupabaseError('Referenced resource not found', 'NOT_FOUND', error)
case 'PGRST116':
throw new SupabaseError('Resource not found', 'NOT_FOUND', error)
default:
throw new SupabaseError(error.message, error.code, error)
}
}
```
### Service with Error Handling
```typescript
async findById(id: string) {
const { data, error } = await supabase
.from(this.tableName)
.select('*')
.eq('id', id)
.single()
if (error) {
handleSupabaseError(error)
}
return data
}
```
## Context7 Query Examples
For latest client documentation:
Topic: "supabase-js typescript client"
Topic: "supabase ssr next.js app router"
Topic: "supabase realtime subscription"
---
Related Modules:
- auth-integration.md - Auth patterns
- realtime-presence.md - Real-time subscriptions
- postgresql-pgvector.md - Database operations