.agents/skills/supabase-expert/SKILL.md
Comprehensive Supabase expert with access to 2,616 official documentation files covering PostgreSQL database, authentication, real-time subscriptions, storage, edge functions, vector embeddings, and all platform features. Invoke when user mentions Supabase, PostgreSQL, database, auth, real-time, storage, edge functions, backend-as-a-service, or pgvector.
npx skillsauth add RomainGRAS42/Procedio-AI supabase-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Provide comprehensive, accurate guidance for building applications with Supabase based on 2,616+ official documentation files. Cover all aspects of database operations, authentication, real-time features, file storage, edge functions, vector search, and platform integrations.
Full access to official Supabase documentation (when available):
docs/supabase_com/Note: Documentation must be pulled separately:
pipx install docpull
docpull https://supabase.com/docs -o .claude/skills/supabase/docs
Major Areas:
Invoke when user mentions:
When answering questions:
Search for specific topics:
# Use Grep to find relevant docs
grep -r "row level security" docs/supabase_com/ --include="*.md"
Find guides:
# Guides are organized by feature
ls docs/supabase_com/guides_*
Check reference docs:
# Reference docs for client libraries
ls docs/supabase_com/reference_*
npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
Environment Variables:
NEXT_PUBLIC_SUPABASE_URL - Your project URL (safe for client)NEXT_PUBLIC_SUPABASE_ANON_KEY - Anonymous/public key (safe for client)SUPABASE_SERVICE_ROLE_KEY - Admin key (server-side only, bypasses RLS)// Insert
const { data, error } = await supabase
.from('posts')
.insert({
title: 'Hello World',
content: 'My first post',
user_id: user.id,
})
.select()
.single();
// Read (with filters)
const { data: posts } = await supabase
.from('posts')
.select('*')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10);
// Update
const { data, error } = await supabase
.from('posts')
.update({ published: true })
.eq('id', postId)
.select()
.single();
// Delete
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId);
// Upsert (insert or update)
const { data, error } = await supabase
.from('profiles')
.upsert({
id: user.id,
name: 'John Doe',
updated_at: new Date().toISOString(),
})
.select();
// Joins
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar),
comments(count)
`)
.eq('published', true);
// Full-text search
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', `'nextjs' & 'supabase'`);
// Range queries
const { data } = await supabase
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lt('created_at', '2024-12-31');
// JSON queries
const { data } = await supabase
.from('posts')
.select('*')
.contains('metadata', { tags: ['tutorial'] });
// Call stored procedure
const { data, error } = await supabase
.rpc('get_user_stats', {
user_id: userId,
});
// Call with filters
const { data } = await supabase
.rpc('search_posts', { search_term: 'supabase' })
.limit(10);
// Email/password signup
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'secure-password',
options: {
data: {
first_name: 'John',
last_name: 'Doe',
},
},
});
// Email/password sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'secure-password',
});
// Magic link (passwordless)
const { data, error } = await supabase.auth.signInWithOtp({
email: '[email protected]',
options: {
emailRedirectTo: 'https://example.com/auth/callback',
},
});
// Phone/SMS
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890',
});
// Google sign in
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
scopes: 'profile email',
},
});
Supported providers:
// Get current user
const { data: { user } } = await supabase.auth.getUser();
// Get session
const { data: { session } } = await supabase.auth.getSession();
// Sign out
const { error } = await supabase.auth.signOut();
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('User signed in:', session.user);
}
if (event === 'SIGNED_OUT') {
console.log('User signed out');
}
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed');
}
});
// Enroll MFA
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp',
friendlyName: 'My Authenticator App',
});
// Verify MFA
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId: data.id,
code: '123456',
});
// List factors
const { data: factors } = await supabase.auth.mfa.listFactors();
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Public read access
CREATE POLICY "Posts are viewable by everyone"
ON posts FOR SELECT
USING (true);
-- Users can insert their own posts
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update only their posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- Users can delete only their posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
-- Conditional access (e.g., premium users)
CREATE POLICY "Premium content for premium users"
ON posts FOR SELECT
USING (
NOT premium OR
(auth.uid() IN (
SELECT user_id FROM subscriptions
WHERE status = 'active'
))
);
-- Get current user ID
auth.uid()
-- Get current JWT
auth.jwt()
-- Access JWT claims
(auth.jwt()->>'role')::text
(auth.jwt()->>'email')::text
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // or 'INSERT', 'UPDATE', 'DELETE'
schema: 'public',
table: 'posts',
},
(payload) => {
console.log('Change received:', payload);
}
)
.subscribe();
// Cleanup
channel.unsubscribe();
// Only listen to specific user's posts
const channel = supabase
.channel('my-posts')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: `user_id=eq.${userId}`,
},
(payload) => {
console.log('New post:', payload.new);
}
)
.subscribe();
const channel = supabase.channel('chat-room');
// Send message
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello!', user: 'John' },
});
// Receive messages
channel.on('broadcast', { event: 'message' }, (payload) => {
console.log('Message:', payload.payload);
});
await channel.subscribe();
const channel = supabase.channel('room-1');
// Track presence
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
console.log('Online users:', Object.keys(state).length);
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', newPresences);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', leftPresences);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString(),
});
}
});
const file = event.target.files[0];
const { data, error } = await supabase.storage
.from('avatars')
.upload(`public/${userId}/avatar.png`, file, {
cacheControl: '3600',
upsert: true,
});
// Upload from base64
const { data, error } = await supabase.storage
.from('avatars')
.upload('file.png', decode(base64String), {
contentType: 'image/png',
});
// Download as blob
const { data, error } = await supabase.storage
.from('avatars')
.download('public/avatar.png');
const url = URL.createObjectURL(data);
// Get public URL (for public buckets)
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('public/avatar.png');
console.log(data.publicUrl);
// Create temporary access URL
const { data, error } = await supabase.storage
.from('private-files')
.createSignedUrl('document.pdf', 3600); // 1 hour
console.log(data.signedUrl);
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('avatar.png', {
transform: {
width: 400,
height: 400,
resize: 'cover', // 'contain', 'cover', 'fill'
quality: 80,
},
});
const { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' },
});
# Install Supabase CLI
npm install -g supabase
# Initialize project
supabase init
# Create function
supabase functions new my-function
// supabase/functions/my-function/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
try {
// Initialize Supabase client
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
);
// Get authenticated user
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
});
}
// Query database
const { data: posts, error } = await supabase
.from('posts')
.select('*')
.eq('user_id', user.id);
if (error) throw error;
return new Response(JSON.stringify({ posts }), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
});
# Deploy single function
supabase functions deploy my-function
# Deploy all functions
supabase functions deploy
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'World' },
});
console.log(data);
-- Enable extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create table with vector column
CREATE TABLE documents (
id BIGSERIAL PRIMARY KEY,
content TEXT,
embedding VECTOR(1536) -- OpenAI ada-002 dimensions
);
-- Create HNSW index for fast similarity search
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// Generate embedding
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: 'Supabase is awesome',
});
const embedding = response.data[0].embedding;
// Store in database
const { data, error } = await supabase
.from('documents')
.insert({
content: 'Supabase is awesome',
embedding,
});
// Find similar documents
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: embedding,
match_threshold: 0.78,
match_count: 10,
});
Similarity search function:
CREATE FUNCTION match_documents (
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (
id BIGINT,
content TEXT,
similarity FLOAT
)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
documents.id,
documents.content,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
// app/posts/page.tsx
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
export default async function PostsPage() {
const supabase = createServerComponentClient({ cookies });
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false });
return (
<div>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
// app/new-post/page.tsx
'use client';
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
import { useState } from 'react';
export default function NewPostPage() {
const supabase = createClientComponentClient();
const [title, setTitle] = useState('');
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const { error } = await supabase
.from('posts')
.insert({ title });
if (error) console.error(error);
};
return <form onSubmit={handleSubmit}>...</form>;
}
// middleware.ts
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
const supabase = createMiddlewareClient({ req, res });
const { data: { session } } = await supabase.auth.getSession();
// Redirect to login if not authenticated
if (!session && req.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', req.url));
}
return res;
}
export const config = {
matcher: ['/dashboard/:path*'],
};
// app/api/posts/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET() {
const supabase = createRouteHandlerClient({ cookies });
const { data: posts } = await supabase
.from('posts')
.select('*');
return NextResponse.json({ posts });
}
export async function POST(request: Request) {
const supabase = createRouteHandlerClient({ cookies });
const body = await request.json();
const { data, error } = await supabase
.from('posts')
.insert(body)
.select()
.single();
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
return NextResponse.json({ post: data });
}
supabase migration new create_posts_table
-- supabase/migrations/20241116000000_create_posts_table.sql
-- Create table
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users NOT NULL,
title TEXT NOT NULL,
content TEXT,
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
);
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Create policies
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id);
-- Create indexes
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_published_idx ON posts(published) WHERE published = true;
-- Create updated_at trigger
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
# Apply migrations locally
supabase db reset
# Push to remote (production)
supabase db push
# Generate types from your database schema
supabase gen types typescript --project-id YOUR_PROJECT_ID > types/supabase.ts
# Or from local development
supabase gen types typescript --local > types/supabase.ts
// lib/supabase/types.ts
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export interface Database {
public: {
Tables: {
posts: {
Row: {
id: string
created_at: string
title: string
content: string | null
user_id: string
published: boolean
}
Insert: {
id?: string
created_at?: string
title: string
content?: string | null
user_id: string
published?: boolean
}
Update: {
id?: string
created_at?: string
title?: string
content?: string | null
user_id?: string
published?: boolean
}
}
profiles: {
Row: {
id: string
name: string | null
avatar_url: string | null
created_at: string
}
Insert: {
id: string
name?: string | null
avatar_url?: string | null
created_at?: string
}
Update: {
id?: string
name?: string | null
avatar_url?: string | null
created_at?: string
}
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
[_ in never]: never
}
}
}
// lib/supabase/client.ts
import { createClient } from '@supabase/supabase-js'
import { Database } from './types'
export const supabase = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Now you get full type safety!
const { data } = await supabase
.from('posts') // ✅ TypeScript knows this table exists
.select('title, content, profiles(name)') // ✅ TypeScript validates columns
.eq('published', true) // ✅ TypeScript validates types
// data is typed as:
// Array<{ title: string; content: string | null; profiles: { name: string | null } }>
// lib/supabase/client.ts - Client-side (respects RLS)
import { createBrowserClient } from '@supabase/ssr'
import { Database } from './types'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/server.ts - Server-side (Next.js App Router)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from './types'
export function createClient() {
const cookieStore = cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {
// Called from Server Component - ignore
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {
// Called from Server Component - ignore
}
},
},
}
)
}
// lib/supabase/admin.ts - Admin client (bypasses RLS)
import { createClient } from '@supabase/supabase-js'
import { Database } from './types'
export const supabaseAdmin = createClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // ⚠️ Server-side only!
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)
// app/posts/page.tsx
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = createClient()
// Fetch data on server (no loading state needed!)
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(*)')
.eq('published', true)
.order('created_at', { ascending: false })
return (
<div>
{posts?.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.profiles?.name}</p>
<div>{post.content}</div>
</article>
))}
</div>
)
}
// app/actions/posts.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
const title = formData.get('title') as string
const content = formData.get('content') as string
const { error } = await supabase
.from('posts')
.insert({
title,
content,
user_id: user.id,
})
if (error) {
throw new Error(error.message)
}
revalidatePath('/posts')
redirect('/posts')
}
export async function updatePost(id: string, formData: FormData) {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')
const { error } = await supabase
.from('posts')
.update({
title: formData.get('title') as string,
content: formData.get('content') as string,
})
.eq('id', id)
.eq('user_id', user.id) // Ensure user owns the post
if (error) throw new Error(error.message)
revalidatePath('/posts')
}
export async function deletePost(id: string) {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) throw new Error('Unauthorized')
const { error } = await supabase
.from('posts')
.delete()
.eq('id', id)
.eq('user_id', user.id)
if (error) throw new Error(error.message)
revalidatePath('/posts')
}
// app/components/PostsList.tsx
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { Database } from '@/lib/supabase/types'
type Post = Database['public']['Tables']['posts']['Row']
export function PostsList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const supabase = createClient()
useEffect(() => {
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: 'published=eq.true',
},
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new as Post, ...prev])
} else if (payload.eventType === 'UPDATE') {
setPosts(prev =>
prev.map(post =>
post.id === payload.new.id ? (payload.new as Post) : post
)
)
} else if (payload.eventType === 'DELETE') {
setPosts(prev => prev.filter(post => post.id !== payload.old.id))
}
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [supabase])
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}
// app/api/posts/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const supabase = createClient()
const { searchParams } = new URL(request.url)
const limit = parseInt(searchParams.get('limit') || '10')
const { data, error } = await supabase
.from('posts')
.select('*')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(limit)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json(data)
}
export async function POST(request: NextRequest) {
const supabase = createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { data, error } = await supabase
.from('posts')
.insert({
...body,
user_id: user.id,
})
.select()
.single()
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json(data)
}
// app/actions/auth.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export async function signUp(formData: FormData) {
const supabase = createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const name = formData.get('name') as string
const { error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name, // Stored in auth.users.raw_user_meta_data
},
emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
},
})
if (error) {
return { error: error.message }
}
return { success: true, message: 'Check your email to confirm your account' }
}
export async function signIn(formData: FormData) {
const supabase = createClient()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}
export async function signOut() {
const supabase = createClient()
await supabase.auth.signOut()
redirect('/')
}
// app/actions/auth.ts
export async function signInWithGoogle() {
const supabase = createClient()
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
})
if (data?.url) {
redirect(data.url)
}
}
export async function signInWithGitHub() {
const supabase = createClient()
const { data } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
scopes: 'read:user user:email',
},
})
if (data?.url) {
redirect(data.url)
}
}
export async function sendMagicLink(email: string) {
const supabase = createClient()
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_URL}/auth/callback`,
},
})
if (error) {
return { error: error.message }
}
return { success: true, message: 'Check your email for the login link' }
}
export async function sendPhoneOTP(phone: string) {
const supabase = createClient()
const { error } = await supabase.auth.signInWithOtp({
phone,
})
if (error) {
return { error: error.message }
}
return { success: true }
}
export async function verifyPhoneOTP(phone: string, token: string) {
const supabase = createClient()
const { error } = await supabase.auth.verifyOtp({
phone,
token,
type: 'sms',
})
if (error) {
return { error: error.message }
}
redirect('/dashboard')
}
// Enable MFA for user
export async function enableMFA() {
const supabase = createClient()
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp',
friendlyName: 'Authenticator App',
})
if (error) throw error
// data.totp.qr_code - QR code to scan
// data.totp.secret - Secret to enter manually
return data
}
// Verify MFA
export async function verifyMFA(factorId: string, code: string) {
const supabase = createClient()
const { data, error } = await supabase.auth.mfa.challengeAndVerify({
factorId,
code,
})
if (error) throw error
return data
}
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const requestUrl = new URL(request.url)
const code = requestUrl.searchParams.get('code')
if (code) {
const supabase = createClient()
await supabase.auth.exchangeCodeForSession(code)
}
// Redirect to dashboard or wherever
return NextResponse.redirect(new URL('/dashboard', request.url))
}
// middleware.ts
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return request.cookies.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
request.cookies.set({
name,
value,
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value,
...options,
})
},
remove(name: string, options: CookieOptions) {
request.cookies.set({
name,
value: '',
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
response.cookies.set({
name,
value: '',
...options,
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// Protect dashboard routes
if (request.nextUrl.pathname.startsWith('/dashboard') && !user) {
return NextResponse.redirect(new URL('/login', request.url))
}
// Redirect authenticated users away from auth pages
if (request.nextUrl.pathname.startsWith('/login') && user) {
return NextResponse.redirect(new URL('/dashboard', request.url))
}
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/login', '/signup'],
}
-- Users can only see published posts or their own drafts
CREATE POLICY "Users can read appropriate posts"
ON posts FOR SELECT
USING (
published = true
OR
auth.uid() = user_id
);
-- Users can update only their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Team-based access
CREATE TABLE teams (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name TEXT NOT NULL
);
CREATE TABLE team_members (
team_id UUID REFERENCES teams,
user_id UUID REFERENCES auth.users,
role TEXT CHECK (role IN ('owner', 'admin', 'member')),
PRIMARY KEY (team_id, user_id)
);
CREATE TABLE team_documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
team_id UUID REFERENCES teams,
title TEXT,
content TEXT
);
-- Only team members can see team documents
CREATE POLICY "Team members can view documents"
ON team_documents FOR SELECT
USING (
team_id IN (
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
)
);
-- Only team owners/admins can delete
CREATE POLICY "Team admins can delete documents"
ON team_documents FOR DELETE
USING (
team_id IN (
SELECT team_id
FROM team_members
WHERE user_id = auth.uid()
AND role IN ('owner', 'admin')
)
);
-- Create helper function
CREATE OR REPLACE FUNCTION is_team_admin(team_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1
FROM team_members
WHERE team_members.team_id = is_team_admin.team_id
AND team_members.user_id = auth.uid()
AND team_members.role IN ('owner', 'admin')
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Use in policy
CREATE POLICY "Admins can update team settings"
ON teams FOR UPDATE
USING (is_team_admin(id))
WITH CHECK (is_team_admin(id));
-- Access custom JWT claims
CREATE POLICY "Premium users can view premium content"
ON premium_content FOR SELECT
USING (
(auth.jwt() -> 'user_metadata' ->> 'subscription_tier') = 'premium'
);
-- Role-based access
CREATE POLICY "Admins have full access"
ON sensitive_data FOR ALL
USING (
(auth.jwt() -> 'user_metadata' ->> 'role') = 'admin'
);
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function OnlineUsers() {
const [onlineUsers, setOnlineUsers] = useState<any[]>([])
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel('online-users')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
const users = Object.values(state).flat()
setOnlineUsers(users)
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('Users joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('Users left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track this user
const { data: { user } } = await supabase.auth.getUser()
if (user) {
await channel.track({
user_id: user.id,
email: user.email,
online_at: new Date().toISOString(),
})
}
}
})
return () => {
supabase.removeChannel(channel)
}
}, [])
return (
<div>
<h3>{onlineUsers.length} users online</h3>
<ul>
{onlineUsers.map((user, i) => (
<li key={i}>{user.email}</li>
))}
</ul>
</div>
)
}
// Cursor tracking
export function CollaborativeCanvas() {
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel('canvas')
channel
.on('broadcast', { event: 'cursor' }, (payload) => {
// Update cursor position
updateCursor(payload.payload)
})
.subscribe()
// Send cursor position
const handleMouseMove = (e: MouseEvent) => {
channel.send({
type: 'broadcast',
event: 'cursor',
payload: { x: e.clientX, y: e.clientY },
})
}
window.addEventListener('mousemove', handleMouseMove)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
supabase.removeChannel(channel)
}
}, [])
return <canvas />
}
// Listen to specific columns
const channel = supabase
.channel('post-changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.123', // Specific row
},
(payload) => {
console.log('Post updated:', payload)
}
)
.subscribe()
// Listen to multiple tables
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
handlePostChange
)
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'comments' },
handleCommentChange
)
.subscribe()
// Upload with transformation
export async function uploadAvatar(file: File, userId: string) {
const supabase = createClient()
const fileExt = file.name.split('.').pop()
const fileName = `${userId}-${Date.now()}.${fileExt}`
const filePath = `avatars/${fileName}`
const { error: uploadError } = await supabase.storage
.from('avatars')
.upload(filePath, file, {
cacheControl: '3600',
upsert: false,
})
if (uploadError) throw uploadError
// Get transformed image URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(filePath, {
transform: {
width: 200,
height: 200,
resize: 'cover',
quality: 80,
},
})
return data.publicUrl
}
// Generate signed URL (expires after 1 hour)
export async function getPrivateFileUrl(path: string) {
const supabase = createClient()
const { data, error } = await supabase.storage
.from('private-files')
.createSignedUrl(path, 3600) // 1 hour
if (error) throw error
return data.signedUrl
}
// Upload to private bucket
export async function uploadPrivateFile(file: File, userId: string) {
const supabase = createClient()
const filePath = `${userId}/${file.name}`
const { error } = await supabase.storage
.from('private-files')
.upload(filePath, file)
if (error) throw error
return filePath
}
-- Enable RLS on storage.objects
CREATE POLICY "Users can upload to their own folder"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can view their own files"
ON storage.objects FOR SELECT
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can update their own files"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own files"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars' AND
(storage.foldername(name))[1] = auth.uid()::text
);
// supabase/functions/hello/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
serve(async (req) => {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${name}!` }),
{
headers: { 'Content-Type': 'application/json' },
},
)
})
// supabase/functions/create-post/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{
global: {
headers: { Authorization: req.headers.get('Authorization')! },
},
}
)
// Get authenticated user
const { data: { user }, error: userError } = await supabaseClient.auth.getUser()
if (userError || !user) {
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
status: 401,
})
}
const { title, content } = await req.json()
const { data, error } = await supabaseClient
.from('posts')
.insert({
title,
content,
user_id: user.id,
})
.select()
.single()
if (error) throw error
return new Response(JSON.stringify(data), {
headers: { 'Content-Type': 'application/json' },
})
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
})
}
})
// supabase/functions/cleanup-old-data/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
// Verify request is from Supabase Cron
const authHeader = req.headers.get('Authorization')
if (authHeader !== `Bearer ${Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')}`) {
return new Response('Unauthorized', { status: 401 })
}
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
)
// Delete old data
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
const { error } = await supabase
.from('temporary_data')
.delete()
.lt('created_at', thirtyDaysAgo.toISOString())
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500,
})
}
return new Response(JSON.stringify({ success: true }))
})
// Configure in Dashboard: Database > Cron Jobs
// Schedule: 0 2 * * * (2am daily)
// HTTP Request: https://your-project.supabase.co/functions/v1/cleanup-old-data
// Client-side
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})
// With auth headers automatically included
const { data: { session } } = await supabase.auth.getSession()
const { data, error } = await supabase.functions.invoke('create-post', {
body: {
title: 'My Post',
content: 'Content here',
},
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
})
-- Enable vector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create table with embedding column
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
content TEXT NOT NULL,
metadata JSONB,
embedding vector(1536), -- For OpenAI ada-002 (1536 dimensions)
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Create index for fast similarity search
CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
-- Or use HNSW for better performance (Postgres 15+)
CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
// lib/embeddings.ts
import { OpenAI } from 'openai'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY!,
})
export async function generateEmbedding(text: string): Promise<number[]> {
const response = await openai.embeddings.create({
model: 'text-embedding-ada-002',
input: text,
})
return response.data[0].embedding
}
// Store document with embedding
export async function storeDocument(content: string, metadata: any) {
const supabase = createClient()
const embedding = await generateEmbedding(content)
const { data, error } = await supabase
.from('documents')
.insert({
content,
metadata,
embedding,
})
.select()
.single()
if (error) throw error
return data
}
// Search similar documents
export async function searchSimilarDocuments(query: string, limit = 5) {
const supabase = createClient()
// Generate embedding for query
const queryEmbedding = await generateEmbedding(query)
// Search with RPC function
const { data, error } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.78, // Minimum similarity
match_count: limit,
})
if (error) throw error
return data
}
// Create the RPC function
CREATE OR REPLACE FUNCTION match_documents (
query_embedding vector(1536),
match_threshold float,
match_count int
)
RETURNS TABLE (
id UUID,
content TEXT,
metadata JSONB,
similarity float
)
LANGUAGE SQL STABLE
AS $$
SELECT
documents.id,
documents.content,
documents.metadata,
1 - (documents.embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
ORDER BY documents.embedding <=> query_embedding
LIMIT match_count;
$$;
export async function ragQuery(question: string) {
// 1. Search for relevant documents
const relevantDocs = await searchSimilarDocuments(question, 5)
// 2. Build context from relevant documents
const context = relevantDocs
.map(doc => doc.content)
.join('\n\n')
// 3. Generate answer with GPT
const completion = await openai.chat.completions.create({
model: 'gpt-4',
messages: [
{
role: 'system',
content: 'You are a helpful assistant. Answer questions based on the provided context.',
},
{
role: 'user',
content: `Context:\n${context}\n\nQuestion: ${question}`,
},
],
})
return {
answer: completion.choices[0].message.content,
sources: relevantDocs,
}
}
-- Get user's post count
CREATE OR REPLACE FUNCTION get_user_post_count(user_id UUID)
RETURNS INTEGER AS $$
SELECT COUNT(*)::INTEGER
FROM posts
WHERE posts.user_id = get_user_post_count.user_id;
$$ LANGUAGE SQL STABLE;
-- Call from TypeScript
const { data, error } = await supabase.rpc('get_user_post_count', {
user_id: userId,
})
-- Auto-update updated_at column
CREATE OR REPLACE FUNCTION handle_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION handle_updated_at();
-- Update post count when post is created/deleted
CREATE OR REPLACE FUNCTION update_post_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE profiles
SET post_count = post_count + 1
WHERE id = NEW.user_id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
UPDATE profiles
SET post_count = post_count - 1
WHERE id = OLD.user_id;
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_post_count_trigger
AFTER INSERT OR DELETE ON posts
FOR EACH ROW
EXECUTE FUNCTION update_post_count();
// Bad: N+1 query problem
const { data: posts } = await supabase.from('posts').select('*')
for (const post of posts) {
const { data: author } = await supabase
.from('profiles')
.select('*')
.eq('id', post.user_id)
.single()
}
// Good: Join in single query
const { data: posts } = await supabase
.from('posts')
.select(`
*,
profiles (
id,
name,
avatar_url
)
`)
// Good: Use specific columns
const { data: posts } = await supabase
.from('posts')
.select('id, title, created_at, profiles(name)') // Only what you need
.eq('published', true)
.order('created_at', { ascending: false })
.limit(20)
-- Index on frequently filtered columns
CREATE INDEX posts_user_id_idx ON posts(user_id);
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
-- Partial index (filtered)
CREATE INDEX posts_published_idx ON posts(published)
WHERE published = true;
-- Composite index
CREATE INDEX posts_user_published_idx ON posts(user_id, published, created_at DESC);
-- Full-text search index
CREATE INDEX posts_content_idx ON posts
USING GIN (to_tsvector('english', content));
// Use connection pooler for serverless (Supavisor)
// Connection string: postgresql://postgres.[project-ref]:[password]@aws-0-us-east-1.pooler.supabase.com:6543/postgres
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{
db: {
schema: 'public',
},
global: {
headers: { 'x-my-custom-header': 'my-app-name' },
},
}
)
// Next.js cache with revalidation
import { unstable_cache } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export const getCachedPosts = unstable_cache(
async () => {
const supabase = createClient()
const { data } = await supabase
.from('posts')
.select('*')
.eq('published', true)
return data
},
['posts'],
{
revalidate: 300, // 5 minutes
tags: ['posts'],
}
)
// Revalidate on mutation
import { revalidateTag } from 'next/cache'
export async function createPost(data: any) {
const supabase = createClient()
await supabase.from('posts').insert(data)
revalidateTag('posts')
}
# Install Supabase CLI
brew install supabase/tap/supabase
# Initialize project
supabase init
# Start local Supabase (Docker required)
supabase start
# This starts:
# - PostgreSQL
# - GoTrue (Auth)
# - Realtime
# - Storage
# - Kong (API Gateway)
# - Studio (Dashboard)
# After supabase start:
API URL: http://localhost:54321
Studio URL: http://localhost:54323
Inbucket URL: http://localhost:54324 # Email testing
# Create migration
supabase migration new add_posts_table
# Edit migration file in supabase/migrations/
# Apply migration locally
supabase db reset
# Push to production
supabase db push
# Pull remote schema
supabase db pull
# Generate TypeScript types
supabase gen types typescript --local > types/database.ts
-- Test as specific user
SET LOCAL ROLE authenticated;
SET LOCAL "request.jwt.claims" TO '{"sub": "user-uuid-here"}';
-- Test query
SELECT * FROM posts;
-- Reset
RESET ROLE;
// tests/posts.test.ts
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY! // For testing
)
describe('Posts', () => {
beforeEach(async () => {
// Clean up
await supabase.from('posts').delete().neq('id', '00000000-0000-0000-0000-000000000000')
})
it('should create post', async () => {
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Test', content: 'Test' })
.select()
.single()
expect(error).toBeNull()
expect(data.title).toBe('Test')
})
})
import { PostgrestError } from '@supabase/supabase-js'
export function handleSupabaseError(error: PostgrestError | null) {
if (!error) return null
// Common error codes
const errorMessages: Record<string, string> = {
'23505': 'This record already exists', // Unique violation
'23503': 'Related record not found', // Foreign key violation
'42P01': 'Table does not exist',
'42501': 'Permission denied',
'PGRST116': 'No rows found',
}
const userMessage = errorMessages[error.code] || error.message
console.error('Supabase error:', {
code: error.code,
message: error.message,
details: error.details,
hint: error.hint,
})
return userMessage
}
// Usage
const { data, error } = await supabase.from('posts').insert(postData)
if (error) {
const message = handleSupabaseError(error)
toast.error(message)
return
}
Row Level Security:
Query Optimization:
.select() to specify needed columns.limit() to cap resultsReal-time Subscriptions:
Authentication:
Storage:
Error Handling:
error object-- Create profiles table
CREATE TABLE profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Trigger function
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, name, avatar_url)
VALUES (
NEW.id,
NEW.raw_user_meta_data->>'name',
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Trigger
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
Need to find something specific?
Search the 2,616 documentation files:
# Search all docs
grep -r "search term" docs/supabase_com/
# Find guides
ls docs/supabase_com/guides_*
# Find API reference
ls docs/supabase_com/reference_*
Common doc locations:
docs/supabase_com/guides_*docs/supabase_com/reference_javascript_*docs/supabase_com/guides_database_*docs/supabase_com/guides_auth_*docs/supabase_com/guides_storage_*npm install @supabase/supabase-jstools
YouTube thumbnail design with specific dimensions, contrast rules, and mobile preview optimization. Covers safe zones, text placement, face expression psychology, and A/B testing. Use for: YouTube thumbnails, video cover images, click-through optimization. Triggers: youtube thumbnail, thumbnail design, video thumbnail, click through rate, ctr optimization, youtube cover, video cover image, thumbnail maker, thumbnail tips, youtube design, video preview image
tools
Web search and content extraction with Tavily and Exa via inference.sh CLI. Apps: Tavily Search, Tavily Extract, Exa Search, Exa Answer, Exa Extract. Capabilities: AI-powered search, content extraction, direct answers, research. Use for: research, RAG pipelines, fact-checking, content aggregation, agents. Triggers: web search, tavily, exa, search api, content extraction, research, internet search, ai search, search assistant, web scraping, rag, perplexity alternative
development
Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
development
Conduct WCAG 2.2 accessibility audits with automated testing, manual verification, and remediation guidance. Use when auditing websites for accessibility, fixing WCAG violations, or implementing accessible design patterns.