Supabase: Complete Backend-as-a-Service Guide
Master Supabase for full-stack development. Learn PostgreSQL database, authentication, real-time subscriptions, storage, and build modern applications.
Moshiour Rahman
Advertisement
What is Supabase?
Supabase is an open-source Firebase alternative built on PostgreSQL. It provides database, authentication, real-time subscriptions, storage, and serverless functions in one platform.
Supabase Features
| Feature | Description |
|---|---|
| Database | PostgreSQL with instant APIs |
| Auth | Email, OAuth, magic links |
| Real-time | WebSocket subscriptions |
| Storage | File uploads and CDN |
| Edge Functions | Serverless TypeScript |
Getting Started
Installation
npm install @supabase/supabase-js
Client Setup
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
export const supabase = createClient(supabaseUrl, supabaseKey)
// With TypeScript types
import { Database } from './database.types'
export const supabase = createClient<Database>(supabaseUrl, supabaseKey)
Database Operations
Basic CRUD
// Create
const { data, error } = await supabase
.from('posts')
.insert({
title: 'Hello World',
content: 'My first post',
author_id: userId
})
.select()
.single()
// Read - single
const { data: post } = await supabase
.from('posts')
.select('*')
.eq('id', postId)
.single()
// Read - multiple with filtering
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({ title: 'Updated Title' })
.eq('id', postId)
.select()
.single()
// Delete
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
Advanced Queries
// Select specific columns with relations
const { data } = await supabase
.from('posts')
.select(`
id,
title,
content,
author:profiles(id, username, avatar_url),
comments(id, content, created_at)
`)
.eq('published', true)
// Filtering
const { data } = await supabase
.from('products')
.select('*')
.gt('price', 100) // greater than
.lt('price', 500) // less than
.gte('stock', 1) // greater than or equal
.in('category', ['electronics', 'gadgets'])
.like('name', '%phone%') // pattern matching
.ilike('name', '%PHONE%') // case insensitive
.is('deleted_at', null) // null check
.neq('status', 'draft') // not equal
// Full-text search
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'react hooks', {
type: 'websearch',
config: 'english'
})
// Range queries
const { data } = await supabase
.from('posts')
.select('*')
.range(0, 9) // First 10 results
// OR conditions
const { data } = await supabase
.from('posts')
.select('*')
.or('status.eq.published,status.eq.featured')
// Count
const { count } = await supabase
.from('posts')
.select('*', { count: 'exact', head: true })
Upsert and Transactions
// Upsert (insert or update)
const { data } = await supabase
.from('users')
.upsert({
id: userId,
email: 'user@example.com',
updated_at: new Date().toISOString()
})
.select()
// Bulk insert
const { data } = await supabase
.from('posts')
.insert([
{ title: 'Post 1', content: 'Content 1' },
{ title: 'Post 2', content: 'Content 2' },
{ title: 'Post 3', content: 'Content 3' }
])
.select()
// RPC (stored procedures)
const { data } = await supabase
.rpc('increment_view_count', { post_id: postId })
Authentication
Email/Password Auth
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword',
options: {
data: {
username: 'johndoe',
full_name: 'John Doe'
}
}
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword'
})
// Sign out
await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
// Get session
const { data: { session } } = await supabase.auth.getSession()
OAuth Providers
// Sign in with OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google', // github, discord, twitter, etc.
options: {
redirectTo: 'http://localhost:3000/auth/callback'
}
})
// Handle callback
// In your callback page
const { data, error } = await supabase.auth.exchangeCodeForSession(code)
Magic Link
// Send magic link
const { error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'http://localhost:3000/welcome'
}
})
Auth State Management
// Listen to auth changes
supabase.auth.onAuthStateChange((event, session) => {
console.log('Auth event:', event)
console.log('Session:', session)
if (event === 'SIGNED_IN') {
// User signed in
} else if (event === 'SIGNED_OUT') {
// User signed out
} else if (event === 'TOKEN_REFRESHED') {
// Token was refreshed
}
})
// React hook example
import { useEffect, useState } from 'react'
import { User } from '@supabase/supabase-js'
export function useUser() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null)
setLoading(false)
})
// Listen for changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user ?? null)
}
)
return () => subscription.unsubscribe()
}, [])
return { user, loading }
}
Real-time Subscriptions
Subscribe to Changes
// Subscribe to all changes on a table
const channel = supabase
.channel('posts-channel')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
console.log('Change received!', payload)
}
)
.subscribe()
// Subscribe to specific events
const channel = supabase
.channel('posts-inserts')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
console.log('New post:', payload.new)
}
)
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'posts' },
(payload) => {
console.log('Updated post:', payload.new)
console.log('Old values:', payload.old)
}
)
.on(
'postgres_changes',
{ event: 'DELETE', schema: 'public', table: 'posts' },
(payload) => {
console.log('Deleted post:', payload.old)
}
)
.subscribe()
// Filter subscriptions
const channel = supabase
.channel('user-posts')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'posts',
filter: `author_id=eq.${userId}`
},
(payload) => {
console.log('User post changed:', payload)
}
)
.subscribe()
// Unsubscribe
await supabase.removeChannel(channel)
Broadcast Messages
// Create a channel for broadcasting
const channel = supabase.channel('room-1')
// Subscribe to broadcasts
channel
.on('broadcast', { event: 'cursor-pos' }, ({ payload }) => {
console.log('Cursor position:', payload)
})
.subscribe()
// Send broadcast
channel.send({
type: 'broadcast',
event: 'cursor-pos',
payload: { x: 100, y: 200 }
})
Presence
// Track user presence
const channel = supabase.channel('online-users')
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,
username: 'johndoe',
online_at: new Date().toISOString()
})
}
})
Storage
File Operations
// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload(`${userId}/avatar.png`, file, {
cacheControl: '3600',
upsert: true
})
// Download file
const { data, error } = await supabase.storage
.from('avatars')
.download(`${userId}/avatar.png`)
// Get public URL
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`)
console.log(data.publicUrl)
// Create signed URL (temporary)
const { data, error } = await supabase.storage
.from('private-files')
.createSignedUrl('document.pdf', 3600) // 1 hour
// List files
const { data, error } = await supabase.storage
.from('avatars')
.list(userId, {
limit: 100,
offset: 0,
sortBy: { column: 'created_at', order: 'desc' }
})
// Delete file
const { error } = await supabase.storage
.from('avatars')
.remove([`${userId}/avatar.png`])
// Move/rename file
const { error } = await supabase.storage
.from('avatars')
.move('old-path/file.png', 'new-path/file.png')
Row Level Security (RLS)
SQL Policies
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: Users can read all published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
-- Policy: Users can only insert their own posts
CREATE POLICY "Users can insert own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = author_id);
-- Policy: Users can only update their own posts
CREATE POLICY "Users can update own posts"
ON posts FOR UPDATE
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);
-- Policy: Users can only delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
-- Policy: Admins can do anything
CREATE POLICY "Admins have full access"
ON posts FOR ALL
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE profiles.id = auth.uid()
AND profiles.role = 'admin'
)
);
Edge Functions
Create Function
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
serve(async (req) => {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${name}!` }),
{ headers: { 'Content-Type': 'application/json' } }
)
})
// Call from client
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'World' }
})
React Integration
// hooks/usePosts.ts
import { useEffect, useState } from 'react'
import { supabase } from '../lib/supabase'
export function usePosts() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchPosts()
// Subscribe to changes
const channel = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
if (payload.eventType === 'INSERT') {
setPosts(prev => [payload.new, ...prev])
} else if (payload.eventType === 'DELETE') {
setPosts(prev => prev.filter(p => p.id !== payload.old.id))
} else if (payload.eventType === 'UPDATE') {
setPosts(prev => prev.map(p =>
p.id === payload.new.id ? payload.new : p
))
}
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])
async function fetchPosts() {
const { data } = await supabase
.from('posts')
.select('*')
.order('created_at', { ascending: false })
setPosts(data || [])
setLoading(false)
}
return { posts, loading }
}
Summary
| Feature | Use Case |
|---|---|
| Database | PostgreSQL with REST API |
| Auth | User authentication |
| Real-time | Live updates |
| Storage | File uploads |
| Edge Functions | Serverless compute |
| RLS | Row-level security |
Supabase provides a complete backend solution for modern applications with PostgreSQL at its core.
Advertisement
Moshiour Rahman
Software Architect & AI Engineer
Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.
Related Articles
MongoDB with Node.js: Complete Database Guide
Master MongoDB with Node.js and Mongoose. Learn CRUD operations, schema design, indexing, aggregation pipelines, and production best practices.
JavaScriptDrizzle ORM: Type-Safe Database for TypeScript
Master Drizzle ORM for TypeScript applications. Learn schema definition, queries, migrations, relations, and build type-safe database layers.
JavaScriptGraphQL API Development: Complete Guide with Node.js
Master GraphQL API development from scratch. Learn schema design, resolvers, queries, mutations, subscriptions, and authentication best practices.
Comments
Comments are powered by GitHub Discussions.
Configure Giscus at giscus.app to enable comments.