| name | supabase |
| description | This skill should be used when the user asks to "integrate Supabase", "set up Supabase auth", "write RLS policies", "use Supabase storage", "subscribe to realtime changes", or mentions "supabase", "supabase auth", "RLS", "row level security", "supabase client", "supabase storage", "realtime", "edge function", "supabase migration". Provides Supabase best practices for Next.js applications including auth, database, storage, realtime, and edge functions. |
| license | MIT |
| metadata | {"author":"Chris Kelley (hello@iwritecode.io)","version":"1.0.0"} |
Supabase Skill
You are a Supabase expert for Next.js applications using the App Router.
Critical Rules
- Always enable RLS on every table — a table without Row Level Security is open to any authenticated user by default
- Use the server-side client for Server Components —
createServerClient from @supabase/ssr reads cookies via Next.js cookies(); it never leaks tokens to the browser
- Use the browser client for Client Components —
createBrowserClient manages the session in the browser; it must never be used in Server Components or Route Handlers
- Never expose the
service_role key on the client — it bypasses RLS entirely; keep it server-only in environment variables prefixed SUPABASE_SERVICE_ROLE_KEY (never NEXT_PUBLIC_)
- Use database migrations for schema changes — never edit schema through the Supabase dashboard in production; use
supabase migration new and commit SQL files to version control
- Always use parameterized queries — the Supabase client does this automatically; never interpolate user input into raw SQL strings
- Refresh sessions in middleware — call
supabase.auth.getUser() in middleware.ts on every request to keep the session cookie fresh
Client Setup
Install the required packages:
npm install @supabase/supabase-js @supabase/ssr
Server Component / Route Handler client — reads and writes cookies via Next.js cookies():
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/supabase'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() { return cookieStore.getAll() },
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {}
},
},
}
)
}
Client Component client — manages the session in the browser:
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/supabase'
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
Read reference/auth-patterns.md for the full middleware setup, sign-in/sign-up flows, OAuth, and protected route patterns.
Database
Always import your generated types for full type safety:
import type { Database } from '@/types/supabase'
Common query patterns:
const supabase = await createClient()
const { data, error } = await supabase
.from('posts')
.select('id, title, created_at')
.eq('user_id', userId)
.order('created_at', { ascending: false })
const { data, error } = await supabase
.from('posts')
.insert({ title, body, user_id: userId })
.select()
.single()
const { error } = await supabase
.from('profiles')
.upsert({ id: userId, display_name: name })
Read reference/database-patterns.md for RLS policy patterns, migrations, RPC functions, joins, and views.
RLS Policies — Quick Reference
CREATE POLICY "authenticated read" ON posts
FOR SELECT TO authenticated USING (true);
CREATE POLICY "owner write" ON posts
FOR ALL TO authenticated USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "admin only" ON admin_logs
FOR SELECT TO authenticated
USING (auth.jwt() ->> 'role' = 'admin');
Read reference/database-patterns.md for full RLS patterns including multi-tenancy, team-based access, and helper functions.
Storage
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.png`, file, { upsert: true })
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`)
const { data, error } = await supabase.storage
.from('documents')
.createSignedUrl(`${userId}/file.pdf`, 60)
Read reference/realtime-storage.md for image transformations, resumable uploads, and storage RLS.
Realtime
'use client'
const channel = supabase
.channel('posts-changes')
.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => setItems(prev => [payload.new as Post, ...prev])
)
.subscribe()
return () => { supabase.removeChannel(channel) }
Read reference/realtime-storage.md for presence, broadcast, and channel cleanup patterns.
Anti-Patterns
- Do not disable RLS — even temporarily; attackers do not wait for you to re-enable it
- Do not use
service_role on the client — it bypasses all security policies; it belongs only in trusted server environments
- Do not skip migrations — dashboard edits to schema are not tracked in version control and will diverge between environments
- Do not store auth state in localStorage — the
@supabase/ssr client handles sessions via HttpOnly cookies; localStorage is not accessible server-side and is vulnerable to XSS
- Do not call
getSession() on the server — use getUser() instead; getSession() does not revalidate the token against the Supabase server
- Do not use
createClient from @supabase/supabase-js in Next.js App Router — use @supabase/ssr; the base client does not integrate with Next.js cookie handling
Related
reference/auth-patterns.md — Sign up, sign in, OAuth, magic link, middleware, protected routes, RBAC
reference/database-patterns.md — Schema design, RLS policies, migrations, RPC functions, joins, views
reference/realtime-storage.md — Realtime subscriptions, presence, broadcast, storage upload/download, signed URLs
- Supabase docs: https://supabase.com/docs
@supabase/ssr docs: https://supabase.com/docs/guides/auth/server-side/nextjs