feature: supabase sdk references (#40)

* rebase and house keeping

* fix supabase sdk reference files after docs review

* update agents.md
This commit is contained in:
Pedro Rodrigues
2026-02-13 15:27:29 +00:00
committed by Pedro Rodrigues
parent 66cfeddf63
commit 0a5b9bfd14
16 changed files with 1749 additions and 5 deletions

View File

@@ -28,8 +28,9 @@ supabase/
|----------|----------|--------|--------| |----------|----------|--------|--------|
| 1 | Database | CRITICAL | `db-` | | 1 | Database | CRITICAL | `db-` |
| 2 | Edge Functions | HIGH | `edge-` | | 2 | Edge Functions | HIGH | `edge-` |
| 3 | Realtime | MEDIUM-HIGH | `realtime-` | | 3 | SDK | HIGH | `sdk-` |
| 3 | Storage | HIGH | `storage-` | | 4 | Realtime | MEDIUM-HIGH | `realtime-` |
| 5 | Storage | HIGH | `storage-` |
Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`). Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md`).
@@ -83,6 +84,21 @@ Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md
- `references/realtime-setup-auth.md` - `references/realtime-setup-auth.md`
- `references/realtime-setup-channels.md` - `references/realtime-setup-channels.md`
**SDK** (`sdk-`):
- `references/sdk-client-browser.md`
- `references/sdk-client-config.md`
- `references/sdk-client-server.md`
- `references/sdk-error-handling.md`
- `references/sdk-framework-nextjs.md`
- `references/sdk-perf-queries.md`
- `references/sdk-perf-realtime.md`
- `references/sdk-query-crud.md`
- `references/sdk-query-filters.md`
- `references/sdk-query-joins.md`
- `references/sdk-query-rpc.md`
- `references/sdk-ts-generation.md`
- `references/sdk-ts-usage.md`
**Storage** (`storage-`): **Storage** (`storage-`):
- `references/storage-access-control.md` - `references/storage-access-control.md`
- `references/storage-cdn-caching.md` - `references/storage-cdn-caching.md`
@@ -94,4 +110,4 @@ Reference files are named `{prefix}-{topic}.md` (e.g., `query-missing-indexes.md
--- ---
*49 reference files across 4 categories* *62 reference files across 5 categories*

View File

@@ -59,6 +59,17 @@ Reference the appropriate resource file based on the user's needs:
| Postgres Changes | `references/realtime-postgres-*.md` | Database change listeners (prefer Broadcast) | | Postgres Changes | `references/realtime-postgres-*.md` | Database change listeners (prefer Broadcast) |
| Patterns | `references/realtime-patterns-*.md` | Cleanup, error handling, React integration | | Patterns | `references/realtime-patterns-*.md` | Cleanup, error handling, React integration |
### SDK (supabase-js)
| Area | Resource | When to Use |
| --------------- | ------------------------------- | ----------------------------------------- |
| Client Setup | `references/sdk-client-*.md` | Browser/server client, SSR, configuration |
| TypeScript | `references/sdk-ts-*.md` | Type generation, using Database types |
| Query Patterns | `references/sdk-query-*.md` | CRUD, filters, joins, RPC calls |
| Error Handling | `references/sdk-error-*.md` | Error types, retries, handling patterns |
| SDK Performance | `references/sdk-perf-*.md` | Query optimization, realtime cleanup |
| Framework | `references/sdk-framework-*.md` | Next.js App Router, middleware setup |
### Storage ### Storage
| Area | Resource | When to Use | | Area | Resource | When to Use |

View File

@@ -15,12 +15,17 @@ queries.
**Impact:** HIGH **Impact:** HIGH
**Description:** Fundamentals, authentication, database access, CORS, routing, error handling, streaming, WebSockets, regional invocations, testing, and limits. **Description:** Fundamentals, authentication, database access, CORS, routing, error handling, streaming, WebSockets, regional invocations, testing, and limits.
## 3. Realtime (realtime) ## 3. SDK (sdk)
**Impact:** HIGH
**Description:** supabase-js client initialization, TypeScript generation, CRUD queries, filters, joins, RPC calls, error handling, performance, and Next.js integration.
## 4. Realtime (realtime)
**Impact:** MEDIUM-HIGH **Impact:** MEDIUM-HIGH
**Description:** Channel setup, Broadcast messaging, Presence tracking, Postgres Changes listeners, cleanup patterns, error handling, and debugging. **Description:** Channel setup, Broadcast messaging, Presence tracking, Postgres Changes listeners, cleanup patterns, error handling, and debugging.
## 3. Storage (storage) ## 5. Storage (storage)
**Impact:** HIGH **Impact:** HIGH
**Description:** File uploads (standard and resumable), downloads, signed URLs, image transformations, CDN caching, access control with RLS policies, and file management operations. **Description:** File uploads (standard and resumable), downloads, signed URLs, image transformations, CDN caching, access control with RLS policies, and file management operations.

View File

@@ -0,0 +1,64 @@
---
title: Browser Client Setup
impact: CRITICAL
impactDescription: Prevents session conflicts and memory leaks from multiple client instances
tags: createBrowserClient, singleton, client-side, ssr, supabase-js
---
## Browser Client Setup
Use `createBrowserClient` from `@supabase/ssr` for client-side code. It implements a singleton pattern internally.
**Incorrect:**
```typescript
// Creates new instance on every render - causes session conflicts
import { createClient } from '@supabase/supabase-js'
function MyComponent() {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
// ...
}
```
**Correct:**
```typescript
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}
// In component - uses singleton
import { createClient } from '@/lib/supabase/client'
function MyComponent() {
const supabase = createClient()
// ...
}
```
## When to Use
- Client Components (`'use client'`)
- Browser-side event handlers
- Realtime subscriptions in the browser
## Key Points
- `createBrowserClient` returns the same instance on subsequent calls
- Do not use `@supabase/auth-helpers-nextjs` (deprecated)
- Session is stored in cookies; requires middleware/proxy setup for server sync
## Related
- [client-server.md](client-server.md)
- [framework-nextjs.md](framework-nextjs.md)

View File

@@ -0,0 +1,114 @@
---
title: Client Configuration Options
impact: MEDIUM-HIGH
impactDescription: Enables custom auth storage, fetch behavior, and schema selection
tags: configuration, auth, fetch, storage, schema, react-native
---
## Client Configuration Options
Pass options to `createClient` for custom behavior.
**Incorrect:**
```typescript
// Missing configuration - session won't persist in React Native
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
// AsyncStorage not configured, detectSessionInUrl causes issues
```
**Correct:**
```typescript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
db: {
schema: 'public', // Default schema for queries
},
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true,
},
global: {
headers: { 'x-app-version': '1.0.0' },
fetch: customFetch,
},
})
```
## Auth Options
| Option | Default | Description |
|--------|---------|-------------|
| `autoRefreshToken` | `true` | Refresh token before expiry |
| `persistSession` | `true` | Store session in storage |
| `detectSessionInUrl` | `true` | Handle OAuth redirect URLs |
| `storage` | `localStorage` | Custom storage adapter |
| `storageKey` | `sb-<ref>-auth-token` | Storage key name |
## Custom Fetch
Wrap fetch for logging, retries, or custom headers:
```typescript
const supabase = createClient(url, key, {
global: {
fetch: (...args) => {
console.log('Supabase request:', args[0])
return fetch(...args)
}
}
})
```
## React Native Setup
React Native requires AsyncStorage and `detectSessionInUrl: false`:
```typescript
import 'react-native-url-polyfill/auto'
import { AppState } from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
auth: {
storage: AsyncStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false, // Important for React Native
},
})
// Manage token refresh based on app state
AppState.addEventListener('change', (state) => {
if (state === 'active') {
supabase.auth.startAutoRefresh()
} else {
supabase.auth.stopAutoRefresh()
}
})
```
## Multiple Schemas
Query different schemas:
```typescript
// Default schema in config
const supabase = createClient(url, key, {
db: { schema: 'api' }
})
// Or use schema() method per-query
const { data } = await supabase.schema('private').from('secrets').select()
```
## Related
- [client-browser.md](client-browser.md)
- [error-handling.md](error-handling.md)

View File

@@ -0,0 +1,123 @@
---
title: Server Client and Proxy Setup
impact: CRITICAL
impactDescription: Prevents auth bypass and ensures session refresh works correctly
tags: createServerClient, proxy, cookies, ssr, server-components
---
## Server Client Setup
Use `createServerClient` from `@supabase/ssr` for Server Components, Route Handlers, and Server Actions. Create a fresh instance per request.
**Incorrect:**
```typescript
// Using deprecated package
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
// Using individual cookie methods
cookies: {
get(name) { return cookieStore.get(name)?.value },
set(name, value, options) { cookieStore.set({ name, value, ...options }) },
remove(name, options) { cookieStore.set({ name, value: '', ...options }) },
}
```
**Correct:**
```typescript
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Called from Server Component - ignore
}
},
},
}
)
}
```
## Proxy for Session Refresh
Token refresh requires a proxy to update cookies on both request and response.
```typescript
// proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function proxy(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Refresh the Auth token by calling getClaims()
await supabase.auth.getClaims()
return supabaseResponse
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
```
## Never Trust getSession() on Server
`getSession()` reads from cookies without validating the JWT.
```typescript
// DANGEROUS - JWT not validated
const { data: { session } } = await supabase.auth.getSession()
// PREFERRED - validates JWT locally with asymmetric keys (no network request)
const { data, error } = await supabase.auth.getClaims()
if (error || !data) return unauthorizedResponse()
const userId = data.claims.sub
// ALTERNATIVE - validates JWT via Auth server round-trip
const { data: { user } } = await supabase.auth.getUser()
```
> **Asymmetric JWTs (recommended):** Stop and ask the user to enable asymmetric JWT signing keys in the Supabase Dashboard under Settings > JWT Signing Keys. With asymmetric keys, `getClaims()` verifies tokens locally via WebCrypto with no network request — significantly faster than `getUser()` which always round-trips to the Auth server.
## Related
- [client-browser.md](client-browser.md)
- [framework-nextjs.md](framework-nextjs.md)

View File

@@ -0,0 +1,125 @@
---
title: Error Handling Patterns
impact: MEDIUM-HIGH
impactDescription: Prevents runtime errors and enables proper error recovery
tags: error, error-handling, retry, FunctionsHttpError, try-catch
---
## Error Handling Patterns
All Supabase operations return `{ data, error }`. Never assume success.
**Incorrect:**
```typescript
// Destructuring only data - error ignored
const { data } = await supabase.from('users').select()
data.forEach(user => console.log(user)) // Crashes if error!
```
**Correct:**
```typescript
const { data, error } = await supabase.from('users').select()
if (error) {
console.error('Failed to fetch users:', error.message)
return
}
// Safe to use data
data.forEach(user => console.log(user))
```
## Error Object Properties
```typescript
if (error) {
console.log(error.message) // Human-readable message
console.log(error.code) // Error code (e.g., 'PGRST116')
console.log(error.details) // Additional details
console.log(error.hint) // Suggested fix
}
```
## Edge Functions Error Types
```typescript
import {
FunctionsHttpError,
FunctionsRelayError,
FunctionsFetchError,
} from '@supabase/supabase-js'
const { data, error } = await supabase.functions.invoke('my-function')
if (error instanceof FunctionsHttpError) {
// Function returned an error response (4xx/5xx)
const errorBody = await error.context.json()
console.error('Function error:', errorBody)
} else if (error instanceof FunctionsRelayError) {
// Network error between client and Supabase
console.error('Relay error:', error.message)
} else if (error instanceof FunctionsFetchError) {
// Could not reach the function
console.error('Fetch error:', error.message)
}
```
## Automatic Retries
Use `fetch-retry` for resilient requests. Only retry on network errors (e.g., Cloudflare 520). Excessive retries can exhaust the Data API connection pool, causing lower throughput and failed requests.
```typescript
import { createClient } from '@supabase/supabase-js'
import fetchRetry from 'fetch-retry'
const retryFetch = fetchRetry(fetch, {
retries: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
retryOn: [520],
})
const supabase = createClient(url, key, {
global: { fetch: retryFetch },
})
```
## Common Error Codes
| Code | Meaning |
|------|---------|
| `PGRST116` | More than 1 or no items returned (with `.single()`) |
| `23505` | Unique constraint violation |
| `23503` | Foreign key violation |
| `42501` | RLS policy violation |
| `42P01` | Table does not exist |
## Handling No Rows
```typescript
// .single() throws if no rows
const { data, error } = await supabase
.from('users')
.select()
.eq('id', userId)
.single()
if (error?.code === 'PGRST116') {
// No user found - handle gracefully
return null
}
// .maybeSingle() returns null instead of error
const { data } = await supabase
.from('users')
.select()
.eq('id', userId)
.maybeSingle()
// data is null if no rows, no error
```
## Related
- [query-crud.md](query-crud.md)
- [client-config.md](client-config.md)

View File

@@ -0,0 +1,224 @@
---
title: Next.js App Router Integration
impact: HIGH
impactDescription: Enables proper SSR auth with session refresh and type-safe queries
tags: nextjs, app-router, server-components, proxy, ssr
---
## Next.js App Router Integration
Complete setup for Next.js 13+ App Router with Supabase.
**Incorrect:**
```typescript
// Using deprecated package and wrong cookie methods
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerComponentClient({ cookies: () => cookieStore })
}
```
**Correct:**
```typescript
// Using @supabase/ssr with getAll/setAll
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(url, key, {
cookies: {
getAll() { return cookieStore.getAll() },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Server Component - ignore
}
},
},
})
}
```
## 1. Install Packages
```bash
npm install @supabase/supabase-js @supabase/ssr
```
## 2. Environment Variables
```bash
# .env.local
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
```
> The publishable key (`sb_publishable_...`) is replacing the legacy `anon` key. Both work during the transition period.
## 3. Create Client Utilities
**Browser Client** (`lib/supabase/client.ts`):
```typescript
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}
```
**Server Client** (`lib/supabase/server.ts`):
```typescript
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Server Component - ignore
}
},
},
}
)
}
```
## 4. Proxy (Required)
```typescript
// proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function proxy(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Refresh the Auth token
// getClaims() validates JWT locally (fast, no network request, requires asymmetric keys)
// getUser() validates via Auth server round-trip (detects logouts/revocations)
await supabase.auth.getUser()
return supabaseResponse
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
}
```
## 5. Usage Examples
**Server Component:**
```typescript
import { createClient } from '@/lib/supabase/server'
export default async function Page() {
const supabase = await createClient()
const { data: posts } = await supabase.from('posts').select()
return <ul>{posts?.map(post => <li key={post.id}>{post.title}</li>)}</ul>
}
```
**Client Component:**
```typescript
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
export default function RealtimePosts() {
const [posts, setPosts] = useState<any[]>([])
useEffect(() => {
const supabase = createClient()
const channel = supabase
.channel('posts')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => setPosts(prev => [...prev, payload.new])
)
.subscribe()
return () => { supabase.removeChannel(channel) }
}, [])
return <ul>{posts.map(post => <li key={post.id}>{post.title}</li>)}</ul>
}
```
**Server Action:**
```typescript
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const supabase = await createClient()
await supabase.from('posts').insert({ title: formData.get('title') })
revalidatePath('/posts')
}
```
## Common Pitfalls
| Pitfall | Solution |
|---------|----------|
| Using `auth-helpers-nextjs` | Use `@supabase/ssr` |
| Individual cookie methods | Use `getAll()`/`setAll()` |
| Trusting `getSession()` | Use `getUser()` (server-verified) or `getClaims()` (local JWT validation, requires asymmetric keys) |
| Missing proxy | Required for session refresh |
| Reusing server client | Create fresh client per request |
## Related
- [client-browser.md](client-browser.md)
- [client-server.md](client-server.md)

View File

@@ -0,0 +1,137 @@
---
title: Query Performance Optimization
impact: HIGH
impactDescription: Reduces data transfer and improves response times
tags: performance, optimization, select, parallel, Promise.all, explain
---
## Query Performance Optimization
Optimize SDK queries for faster responses and lower bandwidth.
## Select Only Needed Columns
**Incorrect:**
```typescript
// Fetches ALL columns, including large blobs
const { data } = await supabase.from('users').select('*')
```
**Correct:**
```typescript
// Fetch only what you need
const { data } = await supabase.from('users').select('id, name, email')
```
## Parallel Queries with Promise.all
**Incorrect:**
```typescript
// Sequential - slow
const users = await supabase.from('users').select()
const posts = await supabase.from('posts').select()
const comments = await supabase.from('comments').select()
```
**Correct:**
```typescript
// Parallel - fast
const [usersResult, postsResult, commentsResult] = await Promise.all([
supabase.from('users').select('id, name'),
supabase.from('posts').select('id, title').limit(10),
supabase.from('comments').select('id, content').limit(20),
])
```
## Use Filters to Reduce Data
```typescript
// Fetch only relevant data
const { data } = await supabase
.from('orders')
.select('id, total, status')
.eq('user_id', userId)
.gte('created_at', startDate)
.order('created_at', { ascending: false })
.limit(50)
```
## Pagination with range()
```typescript
// Page 1 (rows 0-9)
const { data: page1 } = await supabase
.from('products')
.select('id, name, price')
.range(0, 9)
// Page 2 (rows 10-19)
const { data: page2 } = await supabase
.from('products')
.select('id, name, price')
.range(10, 19)
```
## Count Without Fetching Data
```typescript
// Get count only (no row data transferred)
const { count, error } = await supabase
.from('users')
.select('*', { count: 'exact', head: true })
```
## Debug with EXPLAIN
`explain()` is disabled by default. Enable it first (recommended for non-production only):
```sql
alter role authenticator set pgrst.db_plan_enabled to 'true';
notify pgrst, 'reload config';
```
```typescript
// See query execution plan
const { data, error } = await supabase
.from('orders')
.select()
.eq('status', 'pending')
.explain({ analyze: true, verbose: true })
console.log(data) // Execution plan, not rows
```
## Avoid N+1 Queries
**Incorrect:**
```typescript
// N+1: One query per post
const { data: posts } = await supabase.from('posts').select('id, title')
for (const post of posts) {
const { data: comments } = await supabase
.from('comments')
.select()
.eq('post_id', post.id)
}
```
**Correct:**
```typescript
// Single query with join
const { data: posts } = await supabase.from('posts').select(`
id,
title,
comments (id, content)
`)
```
## Related
- [query-joins.md](query-joins.md)
- [perf-realtime.md](perf-realtime.md)

View File

@@ -0,0 +1,143 @@
---
title: Realtime Performance and Cleanup
impact: HIGH
impactDescription: Prevents memory leaks and ensures reliable subscriptions
tags: realtime, subscriptions, cleanup, channels, broadcast, postgres-changes
---
## Realtime Performance and Cleanup
Realtime subscriptions require proper cleanup to prevent memory leaks.
## Basic Subscription
```typescript
const channel = supabase
.channel('messages')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages' },
(payload) => console.log('New message:', payload.new)
)
.subscribe()
```
## React Cleanup Pattern (Critical)
**Incorrect:**
```typescript
// Memory leak - subscription never cleaned up
useEffect(() => {
const supabase = createClient()
supabase
.channel('messages')
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, handler)
.subscribe()
}, [])
```
**Correct:**
```typescript
useEffect(() => {
const supabase = createClient()
const channel = supabase
.channel('messages')
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, handler)
.subscribe()
// Cleanup on unmount
return () => {
supabase.removeChannel(channel)
}
}, [])
```
## Filter Subscriptions
Reduce server load by filtering at the source:
```typescript
const channel = supabase
.channel('user-messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `recipient_id=eq.${userId}`,
},
handleMessage
)
.subscribe()
```
## Connection Status Handling
```typescript
const channel = supabase.channel('my-channel')
channel.subscribe((status) => {
if (status === 'SUBSCRIBED') {
console.log('Connected')
} else if (status === 'CHANNEL_ERROR') {
console.error('Connection error')
} else if (status === 'TIMED_OUT') {
console.log('Connection timed out, retrying...')
} else if (status === 'CLOSED') {
console.log('Connection closed')
}
})
```
## Use Broadcast for Scale
Postgres Changes don't scale horizontally. For high-throughput use cases, use Broadcast from Database. This requires private channels, Realtime Authorization RLS policies, and `setAuth()`:
```sql
-- RLS policy for broadcast authorization
create policy "Authenticated users can receive broadcasts"
on "realtime"."messages"
for select
to authenticated
using ( true );
```
```typescript
// Client subscribes to broadcast (requires authorization setup)
await supabase.realtime.setAuth()
const channel = supabase
.channel('messages-broadcast', {
config: { private: true },
})
.on('broadcast', { event: 'INSERT' }, (payload) => {
console.log('New message:', payload)
})
.subscribe()
```
## Limitations
| Limitation | Description |
|------------|-------------|
| Single thread | Postgres Changes processed on one thread |
| DELETE filters | Cannot filter DELETE events by column |
| RLS per subscriber | 100 subscribers = 100 RLS checks per change |
| Table names | Cannot contain spaces |
## Multiple Tables
```typescript
const channel = supabase
.channel('db-changes')
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, handleMessages)
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'users' }, handleNewUser)
.subscribe()
```
## Related
- [perf-queries.md](perf-queries.md)
- [error-handling.md](error-handling.md)

View File

@@ -0,0 +1,120 @@
---
title: CRUD Operations
impact: HIGH
impactDescription: Core database operations with proper return handling
tags: select, insert, update, delete, upsert, crud
---
## CRUD Operations
All operations return `{ data, error }`. Always check error before using data.
## Select
```typescript
// All rows
const { data, error } = await supabase.from('users').select()
// Specific columns
const { data, error } = await supabase.from('users').select('id, name, email')
// With count
const { count, error } = await supabase
.from('users')
.select('*', { count: 'exact', head: true })
```
## Insert
```typescript
// Basic insert (no data returned)
const { error } = await supabase.from('users').insert({ name: 'Alice' })
// Insert and return the created row
const { data, error } = await supabase
.from('users')
.insert({ name: 'Alice' })
.select()
.single()
// Bulk insert
const { data, error } = await supabase
.from('users')
.insert([
{ name: 'Alice' },
{ name: 'Bob' },
])
.select()
```
## Update
**Incorrect:**
```typescript
// DANGEROUS: Updates ALL rows!
await supabase.from('users').update({ status: 'inactive' })
```
**Correct:**
```typescript
// Always use a filter
const { data, error } = await supabase
.from('users')
.update({ status: 'inactive' })
.eq('id', userId)
.select()
.single()
```
## Upsert
Insert or update based on primary key or unique constraint:
```typescript
const { data, error } = await supabase
.from('users')
.upsert({ id: 1, name: 'Alice', email: 'alice@example.com' })
.select()
.single()
// Upsert on different column
const { data, error } = await supabase
.from('users')
.upsert(
{ email: 'alice@example.com', name: 'Alice' },
{ onConflict: 'email' }
)
.select()
```
## Delete
```typescript
// Delete with filter
const { error } = await supabase.from('users').delete().eq('id', userId)
// Delete and return deleted row
const { data, error } = await supabase
.from('users')
.delete()
.eq('id', userId)
.select()
.single()
```
## single() vs maybeSingle()
```typescript
// Returns error if 0 or >1 rows
.single()
// Returns null if 0 rows, error only if >1 rows
.maybeSingle()
```
## Related
- [query-filters.md](query-filters.md)
- [error-handling.md](error-handling.md)

View File

@@ -0,0 +1,124 @@
---
title: Filters and Modifiers
impact: HIGH
impactDescription: Enables precise data retrieval with proper filter ordering
tags: filters, eq, neq, in, like, order, limit, range, modifiers
---
## Filters and Modifiers
Filters narrow results. Modifiers shape output.
**Incorrect:**
```typescript
// Missing await - query never executes
const { data } = supabase.from('users')
.select('id, name')
.eq('status', 'active')
console.log(data) // undefined!
```
**Correct:**
```typescript
// Always await the query
const { data } = await supabase.from('users')
.select('id, name')
.eq('status', 'active')
.order('created_at', { ascending: false })
.limit(10)
```
## Comparison Filters
```typescript
.eq('column', value) // Equal
.neq('column', value) // Not equal
.gt('column', value) // Greater than
.gte('column', value) // Greater than or equal
.lt('column', value) // Less than
.lte('column', value) // Less than or equal
```
## Special Filters
```typescript
// In array
.in('status', ['active', 'pending'])
// Is null / is not null
.is('deleted_at', null)
.not('deleted_at', 'is', null)
// Pattern matching
.like('name', '%john%') // Case sensitive
.ilike('name', '%john%') // Case insensitive
// Array contains
.contains('tags', ['urgent', 'bug'])
// Text search
.textSearch('content', 'hello world', { type: 'websearch' })
```
## Boolean Logic
```typescript
// OR conditions
.or('status.eq.active,status.eq.pending')
// Complex: OR with AND
.or('role.eq.admin,and(role.eq.user,verified.eq.true)')
```
## Conditional Filters
Build queries dynamically:
```typescript
let query = supabase.from('products').select('*')
if (category) {
query = query.eq('category', category)
}
if (minPrice) {
query = query.gte('price', minPrice)
}
if (maxPrice) {
query = query.lte('price', maxPrice)
}
const { data, error } = await query
```
## Modifiers
```typescript
// Sorting
.order('created_at', { ascending: false })
.order('name') // Ascending by default
// Pagination
.limit(10)
.range(0, 9) // First 10 rows (0-indexed)
// Single row
.single() // Error if not exactly 1 row
.maybeSingle() // null if 0 rows, error if >1
```
## JSON Column Filters
```typescript
// -> returns JSONB (use for non-string comparisons)
.eq('address->postcode', 90210)
// ->> returns text (use for string comparisons)
.eq('address->>city', 'London')
```
## Related
- [query-crud.md](query-crud.md)
- [query-joins.md](query-joins.md)

View File

@@ -0,0 +1,169 @@
---
title: Joins and Relations
impact: HIGH
impactDescription: Enables efficient data fetching with related tables in a single query
tags: joins, relations, foreign-tables, nested-select, inner-join
---
## Joins and Relations
Foreign key relationships are automatically detected. Fetch related data in one query.
**Incorrect:**
```typescript
// N+1 query pattern - separate query for each post's comments
const { data: posts } = await supabase.from('posts').select('id, title')
for (const post of posts) {
const { data: comments } = await supabase
.from('comments')
.select()
.eq('post_id', post.id)
}
```
**Correct:**
```typescript
// Single query with join - fetches all data at once
const { data: posts } = await supabase.from('posts').select(`
id,
title,
comments (id, content, created_at)
`)
```
## Basic Join (One-to-Many)
```typescript
// Fetch posts with their comments
const { data, error } = await supabase.from('posts').select(`
id,
title,
comments (
id,
content,
created_at
)
`)
// Returns: { id, title, comments: [...] }[]
```
## Many-to-One
```typescript
// Fetch comments with their post
const { data, error } = await supabase.from('comments').select(`
id,
content,
post:posts (
id,
title
)
`)
// Returns: { id, content, post: { id, title } }[]
```
## Inner Join
Only return rows with matching related data:
```typescript
const { data, error } = await supabase.from('posts')
.select(`
id,
title,
comments!inner (
id,
content
)
`)
.eq('comments.approved', true)
// Only posts that have approved comments
```
## Multiple Relationships (Aliases)
When a table has multiple FKs to the same table, disambiguate using the column name or constraint name:
```typescript
// Option 1: Column-based disambiguation
const { data, error } = await supabase.from('messages').select(`
id,
content,
from:sender_id (name),
to:receiver_id (name)
`)
// Option 2: Explicit constraint name (recommended for type inference)
const { data, error } = await supabase.from('messages').select(`
id,
content,
from:users!messages_sender_id_fkey (name),
to:users!messages_receiver_id_fkey (name)
`)
```
## Filter on Related Table
```typescript
// Posts where author is active (use table name in dot notation, not alias)
const { data, error } = await supabase.from('posts')
.select(`
id,
title,
author:users!inner (
id,
name
)
`)
.eq('users.status', 'active')
```
## Count Related Rows
```typescript
const { data, error } = await supabase.from('posts').select(`
id,
title,
comments(count)
`)
// Returns: { id, title, comments: [{ count: 5 }] }[]
```
## Nested Relations
```typescript
const { data, error } = await supabase.from('organizations').select(`
id,
name,
teams (
id,
name,
members:users (
id,
name
)
)
`)
```
## Limit on Related Table
```typescript
const { data, error } = await supabase.from('posts')
.select(`
id,
title,
comments (
id,
content
)
`)
.limit(3, { referencedTable: 'comments' })
```
## Related
- [query-filters.md](query-filters.md)
- [ts-usage.md](ts-usage.md)

View File

@@ -0,0 +1,118 @@
---
title: RPC - Calling Postgres Functions
impact: MEDIUM
impactDescription: Enables complex server-side logic via Postgres functions
tags: rpc, postgres-functions, stored-procedures, plpgsql
---
## RPC - Calling Postgres Functions
Call Postgres functions using `rpc()`. Useful for complex queries, transactions, or business logic.
**Incorrect:**
```typescript
// Ignoring error - data might be null
const { data } = await supabase.rpc('calculate_total', { order_id: 123 })
console.log(data.total) // Crashes if RPC failed!
```
**Correct:**
```typescript
// Always check error before using data
const { data, error } = await supabase.rpc('calculate_total', { order_id: 123 })
if (error) {
console.error('RPC failed:', error.message)
return
}
console.log(data.total)
```
## Basic Call
```typescript
// Function: create function hello_world() returns text
const { data, error } = await supabase.rpc('hello_world')
// data: "Hello, World!"
```
## With Arguments
```typescript
// Function: create function add_numbers(a int, b int) returns int
const { data, error } = await supabase.rpc('add_numbers', { a: 5, b: 3 })
// data: 8
```
## Function Returning Table
```typescript
// Function: create function get_active_users() returns setof users
const { data, error } = await supabase.rpc('get_active_users')
// data: [{ id: 1, name: 'Alice' }, ...]
// With filters (applied to returned rows)
const { data, error } = await supabase
.rpc('get_active_users')
.eq('role', 'admin')
.limit(10)
```
## Read-Only Functions (GET Request)
For functions that don't modify data, use GET for better caching:
```typescript
const { data, error } = await supabase.rpc('get_stats', undefined, { get: true })
```
## Array Arguments
```typescript
// Function: create function sum_array(numbers int[]) returns int
const { data, error } = await supabase.rpc('sum_array', {
numbers: [1, 2, 3, 4, 5]
})
// data: 15
```
## Example: Full-Text Search
```typescript
// Function in Postgres
/*
create function search_posts(search_term text)
returns setof posts as $$
select * from posts
where to_tsvector('english', title || ' ' || content)
@@ plainto_tsquery('english', search_term)
$$ language sql;
*/
const { data, error } = await supabase
.rpc('search_posts', { search_term: 'supabase tutorial' })
.limit(20)
```
## Error Handling
```typescript
const { data, error } = await supabase.rpc('risky_operation', { id: 123 })
if (error) {
// error.code - error code (Postgres or PostgREST-specific, e.g. '42501')
// error.message - human-readable description
// error.details - additional context (can be null)
// error.hint - suggested fix (can be null)
console.error('RPC failed:', error.message)
return
}
// Safe to use data
```
## Related
- [query-crud.md](query-crud.md)
- [error-handling.md](error-handling.md)

View File

@@ -0,0 +1,120 @@
---
title: Generate TypeScript Types
impact: HIGH
impactDescription: Enables compile-time type safety for all database operations
tags: typescript, types, codegen, supabase-cli, database.types.ts
---
## Generate TypeScript Types
Generate types from your database schema using the Supabase CLI.
**Incorrect:**
```typescript
// No types - no compile-time safety
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key)
const { data } = await supabase.from('users').select()
// data is 'any' - typos and wrong columns not caught
```
**Correct:**
```typescript
// With generated types - full type safety
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
const supabase = createClient<Database>(url, key)
const { data } = await supabase.from('users').select('id, name')
// data is { id: number; name: string }[] | null
```
## Generate Types with CLI
```bash
# Login first
npx supabase login
# Generate from remote project
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > database.types.ts
# Generate from local development database
npx supabase gen types typescript --local > database.types.ts
```
## Use Types with Client
```typescript
import { createClient } from '@supabase/supabase-js'
import { Database } from './database.types'
const supabase = createClient<Database>(
process.env.SUPABASE_URL!,
process.env.SUPABASE_PUBLISHABLE_KEY!
)
// Queries are now type-safe
const { data } = await supabase.from('users').select('id, name')
// data: { id: number; name: string }[] | null
```
## Generated Type Structure
```typescript
export interface Database {
public: {
Tables: {
users: {
Row: { // SELECT result type
id: number
name: string
created_at: string
}
Insert: { // INSERT payload type
id?: never // Generated columns must not be supplied
name: string // Required
created_at?: string
}
Update: { // UPDATE payload type
id?: never
name?: string
created_at?: string
}
}
}
Enums: {
status: 'active' | 'inactive'
}
}
}
```
## Regenerate After Schema Changes
Run type generation after any migration:
```bash
# After applying migrations
npx supabase db push
npx supabase gen types typescript --local > database.types.ts
```
## CI/CD Integration
Add to your build pipeline:
```yaml
- name: Generate types
run: npx supabase gen types typescript --project-id $PROJECT_REF > database.types.ts
- name: Check for uncommitted type changes
run: git diff --exit-code database.types.ts
```
## Related
- [ts-usage.md](ts-usage.md)
- [query-crud.md](query-crud.md)

View File

@@ -0,0 +1,131 @@
---
title: Using TypeScript Types
impact: HIGH
impactDescription: Provides type-safe access to tables, enums, and complex query results
tags: typescript, Tables, Enums, QueryData, type-helpers
---
## Using TypeScript Types
Use helper types for cleaner code instead of verbose type paths.
**Incorrect:**
```typescript
// Verbose path - hard to read and maintain
type User = Database['public']['Tables']['users']['Row']
type NewUser = Database['public']['Tables']['users']['Insert']
```
**Correct:**
```typescript
// Helper types - clean and concise
import { Tables, TablesInsert } from './database.types'
type User = Tables<'users'>
type NewUser = TablesInsert<'users'>
```
## Tables and Enums Helpers
```typescript
import { Tables, Enums } from './database.types'
// Get row type for a table
type User = Tables<'users'>
// { id: number; name: string; created_at: string }
// Get enum values
type Status = Enums<'status'>
// 'active' | 'inactive'
// Instead of verbose path
type UserVerbose = Database['public']['Tables']['users']['Row']
```
## Insert and Update Types
```typescript
import { TablesInsert, TablesUpdate } from './database.types'
type NewUser = TablesInsert<'users'>
// { id?: number; name: string; created_at?: string }
type UserUpdate = TablesUpdate<'users'>
// { id?: number; name?: string; created_at?: string }
// Use in functions
async function createUser(user: TablesInsert<'users'>) {
return supabase.from('users').insert(user).select().single()
}
```
## QueryData for Complex Queries
Infer types from query definitions, especially for joins:
```typescript
import { QueryData } from '@supabase/supabase-js'
// Define query
const postsWithAuthorQuery = supabase.from('posts').select(`
id,
title,
author:users (
id,
name
)
`)
// Infer type from query (QueryData returns the array type)
type PostsWithAuthor = QueryData<typeof postsWithAuthorQuery>
// { id: number; title: string; author: { id: number; name: string } | null }[]
// Use the type
const { data } = await postsWithAuthorQuery
const posts: PostsWithAuthor = data ?? []
```
## Type Overrides
Override inferred types. Use `Array<>` for array responses, bare type after `.maybeSingle()`:
```typescript
// Partial override (merges with existing types)
const { data } = await supabase
.from('users')
.select()
.overrideTypes<Array<{ status: 'active' | 'inactive' }>>()
// Complete override (replaces types entirely)
const { data } = await supabase
.from('users')
.select('id, metadata')
.overrideTypes<Array<{ id: number; metadata: CustomMetadataType }>, { merge: false }>()
// Single row override (no Array<> needed)
const { data } = await supabase
.from('users')
.select('id, metadata')
.maybeSingle()
.overrideTypes<{ id: number; metadata: CustomMetadataType }>()
```
## Function Parameters
Type RPC function parameters:
```typescript
type FunctionArgs = Database['public']['Functions']['my_function']['Args']
type FunctionReturn = Database['public']['Functions']['my_function']['Returns']
const { data } = await supabase.rpc('my_function', {
arg1: 'value',
} satisfies FunctionArgs)
```
## Related
- [ts-generation.md](ts-generation.md)
- [query-joins.md](query-joins.md)