From 36390c2528ba5d97018aca27d94a72b717395a16 Mon Sep 17 00:00:00 2001 From: Pedro Rodrigues <44656907+Rodriguespn@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:27:29 +0000 Subject: [PATCH] feature: supabase sdk references (#40) * rebase and house keeping * fix supabase sdk reference files after docs review * update agents.md --- skills/supabase/AGENTS.md | 22 +- skills/supabase/SKILL.md | 11 + skills/supabase/references/_sections.md | 9 +- .../supabase/references/sdk-client-browser.md | 64 +++++ .../supabase/references/sdk-client-config.md | 114 +++++++++ .../supabase/references/sdk-client-server.md | 123 ++++++++++ .../supabase/references/sdk-error-handling.md | 125 ++++++++++ .../references/sdk-framework-nextjs.md | 224 ++++++++++++++++++ .../supabase/references/sdk-perf-queries.md | 137 +++++++++++ .../supabase/references/sdk-perf-realtime.md | 143 +++++++++++ skills/supabase/references/sdk-query-crud.md | 120 ++++++++++ .../supabase/references/sdk-query-filters.md | 124 ++++++++++ skills/supabase/references/sdk-query-joins.md | 169 +++++++++++++ skills/supabase/references/sdk-query-rpc.md | 118 +++++++++ .../supabase/references/sdk-ts-generation.md | 120 ++++++++++ skills/supabase/references/sdk-ts-usage.md | 131 ++++++++++ 16 files changed, 1749 insertions(+), 5 deletions(-) create mode 100644 skills/supabase/references/sdk-client-browser.md create mode 100644 skills/supabase/references/sdk-client-config.md create mode 100644 skills/supabase/references/sdk-client-server.md create mode 100644 skills/supabase/references/sdk-error-handling.md create mode 100644 skills/supabase/references/sdk-framework-nextjs.md create mode 100644 skills/supabase/references/sdk-perf-queries.md create mode 100644 skills/supabase/references/sdk-perf-realtime.md create mode 100644 skills/supabase/references/sdk-query-crud.md create mode 100644 skills/supabase/references/sdk-query-filters.md create mode 100644 skills/supabase/references/sdk-query-joins.md create mode 100644 skills/supabase/references/sdk-query-rpc.md create mode 100644 skills/supabase/references/sdk-ts-generation.md create mode 100644 skills/supabase/references/sdk-ts-usage.md diff --git a/skills/supabase/AGENTS.md b/skills/supabase/AGENTS.md index 59e39a4..5ec89e1 100644 --- a/skills/supabase/AGENTS.md +++ b/skills/supabase/AGENTS.md @@ -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* \ No newline at end of file +*62 reference files across 5 categories* \ No newline at end of file diff --git a/skills/supabase/SKILL.md b/skills/supabase/SKILL.md index e8b0b00..451fb55 100644 --- a/skills/supabase/SKILL.md +++ b/skills/supabase/SKILL.md @@ -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 | diff --git a/skills/supabase/references/_sections.md b/skills/supabase/references/_sections.md index b96e73e..a4bb90b 100644 --- a/skills/supabase/references/_sections.md +++ b/skills/supabase/references/_sections.md @@ -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. diff --git a/skills/supabase/references/sdk-client-browser.md b/skills/supabase/references/sdk-client-browser.md new file mode 100644 index 0000000..d9bc67b --- /dev/null +++ b/skills/supabase/references/sdk-client-browser.md @@ -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) diff --git a/skills/supabase/references/sdk-client-config.md b/skills/supabase/references/sdk-client-config.md new file mode 100644 index 0000000..186e540 --- /dev/null +++ b/skills/supabase/references/sdk-client-config.md @@ -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--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) diff --git a/skills/supabase/references/sdk-client-server.md b/skills/supabase/references/sdk-client-server.md new file mode 100644 index 0000000..6486296 --- /dev/null +++ b/skills/supabase/references/sdk-client-server.md @@ -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) diff --git a/skills/supabase/references/sdk-error-handling.md b/skills/supabase/references/sdk-error-handling.md new file mode 100644 index 0000000..355eaa7 --- /dev/null +++ b/skills/supabase/references/sdk-error-handling.md @@ -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) diff --git a/skills/supabase/references/sdk-framework-nextjs.md b/skills/supabase/references/sdk-framework-nextjs.md new file mode 100644 index 0000000..8c75af2 --- /dev/null +++ b/skills/supabase/references/sdk-framework-nextjs.md @@ -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 +} +``` + +**Client Component:** + +```typescript +'use client' +import { createClient } from '@/lib/supabase/client' +import { useEffect, useState } from 'react' + +export default function RealtimePosts() { + const [posts, setPosts] = useState([]) + + 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 +} +``` + +**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) diff --git a/skills/supabase/references/sdk-perf-queries.md b/skills/supabase/references/sdk-perf-queries.md new file mode 100644 index 0000000..f60dc91 --- /dev/null +++ b/skills/supabase/references/sdk-perf-queries.md @@ -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) diff --git a/skills/supabase/references/sdk-perf-realtime.md b/skills/supabase/references/sdk-perf-realtime.md new file mode 100644 index 0000000..50bf183 --- /dev/null +++ b/skills/supabase/references/sdk-perf-realtime.md @@ -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) diff --git a/skills/supabase/references/sdk-query-crud.md b/skills/supabase/references/sdk-query-crud.md new file mode 100644 index 0000000..e110ff0 --- /dev/null +++ b/skills/supabase/references/sdk-query-crud.md @@ -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) diff --git a/skills/supabase/references/sdk-query-filters.md b/skills/supabase/references/sdk-query-filters.md new file mode 100644 index 0000000..5b15f5d --- /dev/null +++ b/skills/supabase/references/sdk-query-filters.md @@ -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) diff --git a/skills/supabase/references/sdk-query-joins.md b/skills/supabase/references/sdk-query-joins.md new file mode 100644 index 0000000..65ce90d --- /dev/null +++ b/skills/supabase/references/sdk-query-joins.md @@ -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) diff --git a/skills/supabase/references/sdk-query-rpc.md b/skills/supabase/references/sdk-query-rpc.md new file mode 100644 index 0000000..07e7d47 --- /dev/null +++ b/skills/supabase/references/sdk-query-rpc.md @@ -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) diff --git a/skills/supabase/references/sdk-ts-generation.md b/skills/supabase/references/sdk-ts-generation.md new file mode 100644 index 0000000..4b088b8 --- /dev/null +++ b/skills/supabase/references/sdk-ts-generation.md @@ -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(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( + 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) diff --git a/skills/supabase/references/sdk-ts-usage.md b/skills/supabase/references/sdk-ts-usage.md new file mode 100644 index 0000000..57cd301 --- /dev/null +++ b/skills/supabase/references/sdk-ts-usage.md @@ -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 +// { 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>() + +// Complete override (replaces types entirely) +const { data } = await supabase + .from('users') + .select('id, metadata') + .overrideTypes, { 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)