mirror of
https://github.com/supabase/agent-skills.git
synced 2026-03-27 10:09:26 +08:00
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:
committed by
Pedro Rodrigues
parent
5254cf64ef
commit
36390c2528
@@ -28,8 +28,9 @@ supabase/
|
||||
|----------|----------|--------|--------|
|
||||
| 1 | Database | CRITICAL | `db-` |
|
||||
| 2 | Edge Functions | HIGH | `edge-` |
|
||||
| 3 | Realtime | MEDIUM-HIGH | `realtime-` |
|
||||
| 3 | Storage | HIGH | `storage-` |
|
||||
| 3 | SDK | HIGH | `sdk-` |
|
||||
| 4 | Realtime | MEDIUM-HIGH | `realtime-` |
|
||||
| 5 | Storage | HIGH | `storage-` |
|
||||
|
||||
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-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-`):
|
||||
- `references/storage-access-control.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*
|
||||
@@ -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) |
|
||||
| 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
|
||||
|
||||
| Area | Resource | When to Use |
|
||||
|
||||
@@ -15,12 +15,17 @@ queries.
|
||||
**Impact:** HIGH
|
||||
**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
|
||||
**Description:** Channel setup, Broadcast messaging, Presence tracking, Postgres Changes listeners, cleanup patterns, error handling, and debugging.
|
||||
|
||||
## 3. Storage (storage)
|
||||
## 5. Storage (storage)
|
||||
|
||||
**Impact:** HIGH
|
||||
**Description:** File uploads (standard and resumable), downloads, signed URLs, image transformations, CDN caching, access control with RLS policies, and file management operations.
|
||||
|
||||
64
skills/supabase/references/sdk-client-browser.md
Normal file
64
skills/supabase/references/sdk-client-browser.md
Normal 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)
|
||||
114
skills/supabase/references/sdk-client-config.md
Normal file
114
skills/supabase/references/sdk-client-config.md
Normal 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)
|
||||
123
skills/supabase/references/sdk-client-server.md
Normal file
123
skills/supabase/references/sdk-client-server.md
Normal 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)
|
||||
125
skills/supabase/references/sdk-error-handling.md
Normal file
125
skills/supabase/references/sdk-error-handling.md
Normal 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)
|
||||
224
skills/supabase/references/sdk-framework-nextjs.md
Normal file
224
skills/supabase/references/sdk-framework-nextjs.md
Normal 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)
|
||||
137
skills/supabase/references/sdk-perf-queries.md
Normal file
137
skills/supabase/references/sdk-perf-queries.md
Normal 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)
|
||||
143
skills/supabase/references/sdk-perf-realtime.md
Normal file
143
skills/supabase/references/sdk-perf-realtime.md
Normal 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)
|
||||
120
skills/supabase/references/sdk-query-crud.md
Normal file
120
skills/supabase/references/sdk-query-crud.md
Normal 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)
|
||||
124
skills/supabase/references/sdk-query-filters.md
Normal file
124
skills/supabase/references/sdk-query-filters.md
Normal 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)
|
||||
169
skills/supabase/references/sdk-query-joins.md
Normal file
169
skills/supabase/references/sdk-query-joins.md
Normal 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)
|
||||
118
skills/supabase/references/sdk-query-rpc.md
Normal file
118
skills/supabase/references/sdk-query-rpc.md
Normal 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)
|
||||
120
skills/supabase/references/sdk-ts-generation.md
Normal file
120
skills/supabase/references/sdk-ts-generation.md
Normal 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)
|
||||
131
skills/supabase/references/sdk-ts-usage.md
Normal file
131
skills/supabase/references/sdk-ts-usage.md
Normal 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)
|
||||
Reference in New Issue
Block a user