| name | supabase |
| description | Supabase open-source Firebase alternative with Postgres, authentication, storage, and realtime subscriptions. Use when building full-stack applications requiring integrated backend services with Next.js, React, or Vue. |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":{"summary":"Supabase open-source Firebase alternative with Postgres, authentication, storage, and realtime subscriptions. Use when building full-stack applications requiring integrated backend services with Ne...","when_to_use":"When working with supabase-backend-platform or related functionality.","quick_start":"1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."}} |
Supabase Backend Platform Skill
progressive_disclosure:
entry_point:
summary: "Open-source Firebase alternative with Postgres, authentication, storage, and realtime"
when_to_use:
- "When building full-stack applications"
- "When auth, database, and storage are required"
- "When realtime subscriptions are needed"
- "When using Next.js, React, or Vue"
quick_start:
- "Create project on Supabase console"
- "npm install @supabase/supabase-js"
- "Initialize client with URL and anon key"
- "Use auth, database, storage, realtime"
token_estimate:
entry: 80-95
full: 5000-6000
Supabase Fundamentals
What is Supabase?
Open-source Firebase alternative built on:
- Postgres Database: Full SQL database with PostgREST API
- Authentication: Built-in auth with multiple providers
- Storage: File storage with image transformations
- Realtime: WebSocket subscriptions to database changes
- Edge Functions: Serverless functions on Deno runtime
- Row Level Security: Postgres RLS for data access control
Project Setup
npm install @supabase/supabase-js
npm install -D supabase
npm install -D @supabase/supabase-js
Client Initialization
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
import { Database } from '@/types/supabase'
export const supabase = createClient<Database>(
supabaseUrl,
supabaseAnonKey
)
Database Operations
PostgREST API Basics
Supabase auto-generates REST API from Postgres schema:
const { data, error } = await supabase
.from('posts')
.select('*')
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles(name, avatar),
comments(count)
`)
const { data, error } = await supabase
.from('posts')
.insert({ title: 'Hello', content: 'World' })
.select()
.single()
const { data } = await supabase
.from('posts')
.update({ status: 'published' })
.eq('id', postId)
.select()
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
const { data } = await supabase
.from('posts')
.upsert({ id: 1, title: 'Updated' })
.select()
Advanced Queries
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'postgresql', {
type: 'websearch',
config: 'english'
})
const { data } = await supabase
.from('posts')
.select('*')
.gte('created_at', '2024-01-01')
.lte('created_at', '2024-12-31')
const { data } = await supabase
.from('posts')
.select('*')
.contains('tags', ['postgres', 'supabase'])
const { data } = await supabase
.from('users')
.select('*')
.eq('metadata->theme', 'dark')
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
const pageSize = 10
const page = 2
const { data } = await supabase
.from('posts')
.select('*')
.range(page * pageSize, (page + 1) * pageSize - 1)
Database Functions and RPC
const { data, error } = await supabase
.rpc('get_trending_posts', {
days: 7,
min_score: 10
})
Authentication
Email/Password Authentication
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'secure-password',
options: {
data: {
name: 'John Doe',
avatar_url: 'https://...'
}
}
})
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'secure-password'
})
const { error } = await supabase.auth.signOut()
const { data: { user } } = await supabase.auth.getUser()
const { data: { session } } = await supabase.auth.getSession()
OAuth Providers
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'http://localhost:3000/auth/callback',
scopes: 'repo user'
}
})
Magic Links
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'http://localhost:3000/auth/callback'
}
})
const { data, error } = await supabase.auth.verifyOtp({
email: 'user@example.com',
token: '123456',
type: 'email'
})
Phone Authentication
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+1234567890'
})
const { data, error } = await supabase.auth.verifyOtp({
phone: '+1234567890',
token: '123456',
type: 'sms'
})
Auth State Management
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')
}
})
const { data, error } = await supabase.auth.updateUser({
data: { theme: 'dark' }
})
const { data, error } = await supabase.auth.updateUser({
password: 'new-password'
})
Row Level Security (RLS)
RLS Fundamentals
Postgres Row Level Security controls data access at the database level:
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
Common RLS Patterns
CREATE POLICY "Anyone can view posts"
ON posts FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can create posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
CREATE POLICY "Users can view org data"
ON documents FOR SELECT
USING (
organization_id IN (
SELECT organization_id
FROM memberships
WHERE user_id = auth.uid()
)
);
CREATE POLICY "Admins can do anything"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid()
AND role = 'admin'
)
);
CREATE POLICY "View published or scheduled posts"
ON posts FOR SELECT
USING (
status = 'published'
OR (status = 'scheduled' AND publish_at <= NOW())
);
RLS Helper Functions
SELECT auth.uid();
SELECT auth.jwt();
SELECT auth.jwt()->>'email';
SELECT auth.jwt()->'app_metadata'->>'role';
Storage
File Upload
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
cacheControl: '3600',
upsert: false
})
const { data, error } = await supabase.storage
.from('avatars')
.upload('public/avatar1.png', file, {
onUploadProgress: (progress) => {
console.log(`${progress.loaded}/${progress.total}`)
}
})
const { data, error } = await supabase.storage
.from('avatars')
.uploadToSignedUrl('path', token, file)
File Operations
const { data, error } = await supabase.storage
.from('avatars')
.download('public/avatar1.png')
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('public/avatar1.png')
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('private/document.pdf', 3600)
const { data, error } = await supabase.storage
.from('avatars')
.list('public', {
limit: 100,
offset: 0,
sortBy: { column: 'name', order: 'asc' }
})
const { data, error } = await supabase.storage
.from('avatars')
.remove(['public/avatar1.png'])
const { data, error } = await supabase.storage
.from('avatars')
.move('public/avatar1.png', 'public/avatar2.png')
Image Transformations
const { data } = supabase.storage
.from('avatars')
.getPublicUrl('avatar1.png', {
transform: {
width: 200,
height: 200,
resize: 'cover',
quality: 80
}
})
Storage RLS
CREATE POLICY "Avatar images are publicly accessible"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars' AND (storage.foldername(name))[1] = 'public');
CREATE POLICY "Users can upload their own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
CREATE POLICY "Users can delete their own avatar"
ON storage.objects FOR DELETE
USING (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
Realtime Subscriptions
Database Changes
const channel = supabase
.channel('posts-insert')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('New post:', payload.new)
}
)
.subscribe()
const channel = supabase
.channel('posts-update')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('Updated:', payload.new)
console.log('Previous:', payload.old)
}
)
.subscribe()
const channel = supabase
.channel('posts-all')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('Change:', payload)
}
)
.subscribe()
supabase.removeChannel(channel)
Presence (Track Online Users)
const channel = supabase.channel('room-1')
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', state)
})
.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()
})
}
})
await channel.untrack()
Broadcast (Send Messages)
const channel = supabase.channel('chat-room')
channel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('Message:', payload)
})
.subscribe()
await channel.send({
type: 'broadcast',
event: 'message',
payload: { text: 'Hello', user: 'John' }
})
Edge Functions
Edge Function Basics
Serverless functions on Deno runtime:
supabase functions new my-function
supabase functions serve
supabase functions deploy my-function
Edge Function Example
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
serve(async (req) => {
try {
const authHeader = req.headers.get('Authorization')!
const supabase = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: authHeader } } }
)
const { data: { user }, error } = await supabase.auth.getUser()
if (error) throw error
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', user.id)
return new Response(
JSON.stringify({ data }),
{ headers: { 'Content-Type': 'application/json' } }
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
})
Invoke Edge Function
const { data, error } = await supabase.functions.invoke('my-function', {
body: { name: 'John' }
})
const { data, error } = await supabase.functions.invoke('my-function', {
headers: {
Authorization: `Bearer ${session.access_token}`
},
body: { name: 'John' }
})
Next.js Integration
App Router Setup
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
import { createServerClient, type CookieOptions } 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_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
cookieStore.set({ name, value, ...options })
},
remove(name: string, options: CookieOptions) {
cookieStore.set({ name, value: '', ...options })
},
},
}
)
}
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(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 })
},
},
}
)
await supabase.auth.getUser()
return response
}
Middleware
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
Server Component
import { createClient } from '@/lib/supabase/server'
export default async function PostsPage() {
const supabase = await createClient()
const { data: posts } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
return (
<div>
{posts?.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
)
}
Client Component
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'
export function NewPost() {
const [title, setTitle] = useState('')
const supabase = createClient()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (!error) {
setTitle('')
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Post title"
/>
<button>Create</button>
</form>
)
}
Server Actions
'use server'
import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'
export async function createPost(formData: FormData) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: 'Not authenticated' }
}
const title = formData.get('title') as string
const { error } = await supabase
.from('posts')
.insert({ title, author_id: user.id })
if (error) {
return { error: error.message }
}
revalidatePath('/posts')
return { success: true }
}
TypeScript Type Generation
Generate Types from Database
npm install -D supabase
npx supabase login
npx supabase link --project-ref your-project-ref
npx supabase gen types typescript --project-id your-project-ref > types/supabase.ts
npx supabase gen types typescript --local > types/supabase.ts
Use Generated Types
export type Database = {
public: {
Tables: {
posts: {
Row: {
id: string
title: string
content: string | null
author_id: string
created_at: string
}
Insert: {
id?: string
title: string
content?: string | null
author_id: string
created_at?: string
}
Update: {
id?: string
title?: string
content?: string | null
author_id?: string
created_at?: string
}
}
}
}
}
import { createClient } from '@supabase/supabase-js'
import { Database } from '@/types/supabase'
const supabase = createClient<Database>(url, key)
const { data } = await supabase
.from('posts')
.select('title, content')
.single()
Supabase CLI and Local Development
Setup Local Development
npx supabase init
npx supabase start
npx supabase stop
npx supabase db reset
npx supabase status
Database Migrations
npx supabase migration new create_posts_table
npx supabase db push
npx supabase db pull
npx supabase db diff
Migration Example
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title TEXT NOT NULL,
content TEXT,
author_id UUID NOT NULL REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'draft',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can view published posts"
ON posts FOR SELECT
USING (status = 'published');
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id);
CREATE INDEX posts_author_id_idx ON posts(author_id);
CREATE INDEX posts_status_idx ON posts(status);
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON posts
FOR EACH ROW
EXECUTE FUNCTION moddatetime(updated_at);
Security Best Practices
API Key Management
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { persistSession: false } }
)
RLS Best Practices
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
SET request.jwt.claims.sub = 'user-id';
SELECT * FROM posts;
Input Validation
function validatePost(data: unknown) {
const schema = z.object({
title: z.string().min(1).max(200),
content: z.string().max(10000).optional()
})
return schema.parse(data)
}
serve(async (req) => {
const body = await req.json()
try {
const validated = validatePost(body)
} catch (error) {
return new Response(
JSON.stringify({ error: 'Invalid input' }),
{ status: 400 }
)
}
})
Environment Variables
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ...
Production Deployment
Database Optimization
CREATE INDEX posts_created_at_idx ON posts(created_at DESC);
CREATE INDEX posts_author_status_idx ON posts(author_id, status);
CREATE INDEX posts_title_search_idx ON posts
USING GIN (to_tsvector('english', title));
EXPLAIN ANALYZE
SELECT * FROM posts WHERE author_id = 'xxx';
VACUUM ANALYZE posts;
Connection Pooling
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(url, key, {
db: {
schema: 'public',
},
auth: {
persistSession: true,
autoRefreshToken: true,
},
global: {
headers: { 'x-my-custom-header': 'my-value' },
},
})
Monitoring
const supabase = createClient(url, key, {
global: {
fetch: async (url, options) => {
console.log('Query:', url)
return fetch(url, options)
}
}
})
Backup Strategy
pg_dump -h db.xxx.supabase.co -U postgres -d postgres > backup.sql
psql -h db.xxx.supabase.co -U postgres -d postgres < backup.sql
Supabase vs Firebase
Similarities
- Backend-as-a-Service platform
- Authentication with multiple providers
- Realtime data synchronization
- File storage
- Serverless functions
- Generous free tier
Key Differences
Database
- Supabase: PostgreSQL (SQL, full control)
- Firebase: Firestore (NoSQL, limited queries)
Queries
- Supabase: Full SQL, joins, aggregations
- Firebase: Limited filtering, no joins
Security
- Supabase: Row Level Security (Postgres native)
- Firebase: Security Rules (custom syntax)
Open Source
- Supabase: Fully open source, self-hostable
- Firebase: Proprietary, Google-hosted only
Pricing
- Supabase: Compute-based, predictable
- Firebase: Usage-based, can spike
Ecosystem
- Supabase: Postgres ecosystem (extensions, tools)
- Firebase: Google Cloud Platform integration
Migration Considerations
const snapshot = await db
.collection('posts')
.where('status', '==', 'published')
.orderBy('createdAt', 'desc')
.limit(10)
.get()
const { data } = await supabase
.from('posts')
.select('*')
.eq('status', 'published')
.order('created_at', { ascending: false })
.limit(10)
const { data } = await supabase
.from('posts')
.select(`
*,
author:profiles!inner(name),
comments(count)
`)
.gte('created_at', startDate)
.lte('created_at', endDate)
.order('created_at', { ascending: false })
Advanced Patterns
Optimistic Updates
'use client'
import { useState, useOptimistic } from 'react'
import { createClient } from '@/lib/supabase/client'
export function PostList({ initialPosts }: { initialPosts: Post[] }) {
const [posts, setPosts] = useState(initialPosts)
const [optimisticPosts, addOptimisticPost] = useOptimistic(
posts,
(state, newPost: Post) => [...state, newPost]
)
const supabase = createClient()
const createPost = async (title: string) => {
const tempPost = {
id: crypto.randomUUID(),
title,
created_at: new Date().toISOString()
}
addOptimisticPost(tempPost)
const { data } = await supabase
.from('posts')
.insert({ title })
.select()
.single()
if (data) {
setPosts([...posts, data])
}
}
return (
<div>
{optimisticPosts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Infinite Scroll
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
const PAGE_SIZE = 20
export function InfinitePostList() {
const [posts, setPosts] = useState<Post[]>([])
const [page, setPage] = useState(0)
const [hasMore, setHasMore] = useState(true)
const supabase = createClient()
useEffect(() => {
const loadMore = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
.order('created_at', { ascending: false })
if (data) {
setPosts([...posts, ...data])
setHasMore(data.length === PAGE_SIZE)
}
}
loadMore()
}, [page])
return (
<div>
{posts.map((post) => (
<div key={post.id}>{post.title}</div>
))}
{hasMore && (
<button onClick={() => setPage(page + 1)}>
Load More
</button>
)}
</div>
)
}
Debounced Search
'use client'
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
import { useDebounce } from '@/hooks/use-debounce'
export function SearchPosts() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<Post[]>([])
const debouncedQuery = useDebounce(query, 300)
const supabase = createClient()
useEffect(() => {
if (!debouncedQuery) {
setResults([])
return
}
const search = async () => {
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', debouncedQuery)
.limit(10)
if (data) setResults(data)
}
search()
}, [debouncedQuery])
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search posts..."
/>
{results.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Summary
Supabase provides a complete backend platform with:
- Postgres Database with REST and GraphQL APIs
- Built-in Authentication with multiple providers
- Row Level Security for granular access control
- File Storage with image transformations
- Realtime Subscriptions for live updates
- Edge Functions for serverless compute
- Next.js Integration with Server and Client Components
- TypeScript Support with auto-generated types
- Local Development with Supabase CLI
- Production Ready with monitoring and backups
Use Supabase when a full-featured backend with the power of Postgres, built-in auth, and realtime capabilities is needed, all with excellent TypeScript and Next.js integration.