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
66cfeddf63
commit
0a5b9bfd14
@@ -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*
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
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