Introduction
Supabase is an open-source Firebase alternative that provides a complete backend for modern applications. Built on PostgreSQL, it offers databases, authentication, real-time subscriptions, storage, and serverless functions. This comprehensive guide covers everything you need to build powerful applications with Supabase.
Understanding Supabase
What is Supabase?
Supabase is an “open source Firebase” that gives you a scalable backend without managing infrastructure. It combines PostgreSQL’s power with modern developer experience.
graph TB
subgraph "Supabase Stack"
DB[(PostgreSQL)]
Auth[Authentication]
Realtime[Real-time]
Storage[Storage]
Edge[Edge Functions]
API[Auto API]
end
subgraph "Your App"
Web[Web App]
Mobile[Mobile App]
end
Web --> API
Mobile --> API
API --> DB
API --> Auth
API --> Realtime
API --> Storage
API --> Edge
Core Features
| Feature | Description |
|---|---|
| PostgreSQL Database | Full-featured relational database |
| Authentication | Email, OAuth, magic links, phone |
| Real-time | Live subscriptions to database changes |
| Storage | File storage with CDN |
| Edge Functions | Serverless TypeScript functions |
| Auto API | RESTful and GraphQL from your schema |
Getting Started
Quick Setup
# Install Supabase CLI
npm install -g supabase
# Initialize new project
supabase init
# Start local development
supabase start
# Or create project at supabase.com
Connection String
// Supabase client
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'https://your-project.supabase.co'
const supabaseKey = 'your-anon-key'
const supabase = createClient(supabaseUrl, supabaseKey)
Database
Schema Design
-- Create tables
CREATE TABLE profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
username TEXT UNIQUE,
full_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE posts (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
user_id UUID REFERENCES profiles(id) NOT NULL,
title TEXT NOT NULL,
content TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE comments (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
post_id BIGINT REFERENCES posts(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES profiles(id) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Enable Row Level Security
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
Row Level Security (RLS)
-- Profiles: Users can only update their own profile
CREATE POLICY "Users can update own profile"
ON profiles
FOR UPDATE
USING (auth.uid() = id);
-- Profiles: Everyone can read
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles
FOR SELECT
USING (true);
-- Posts: Users can only see their own and public posts
CREATE POLICY "Users can view all posts"
ON posts
FOR SELECT
USING (
user_id = auth.uid()
OR EXISTS (SELECT 1 FROM profiles WHERE id = auth.uid() AND is_public = true)
);
-- Posts: Users can insert their own
CREATE POLICY "Users can create posts"
ON posts
FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Comments: Anyone can read
CREATE POLICY "Anyone can read comments"
ON comments
FOR SELECT
USING (true);
-- Comments: Auth users can create
CREATE POLICY "Auth users can create comments"
ON comments
FOR INSERT
WITH CHECK (auth.uid() = user_id);
Queries from Client
// Select data
const { data: profiles, error } = await supabase
.from('profiles')
.select('username, full_name')
// Filter
const { data: posts } = await supabase
.from('posts')
.select('*, profiles(username)')
.eq('user_id', userId)
.order('created_at', { ascending: false })
.limit(10)
// Insert
const { data, error } = await supabase
.from('posts')
.insert([
{
title: 'My Post',
content: 'Hello World',
user_id: user.id
}
])
// Update
const { data, error } = await supabase
.from('profiles')
.update({ full_name: 'John Doe' })
.eq('id', user.id)
// Delete
const { error } = await supabase
.from('posts')
.delete()
.eq('id', postId)
PostgreSQL Functions
-- Create function to get post with author
CREATE OR REPLACE FUNCTION get_posts_with_authors()
RETURNS SETOF posts AS $$
BEGIN
RETURN QUERY
SELECT p.*
FROM posts p
ORDER BY p.created_at DESC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Call from client
const { data } = await supabase
.rpc('get_posts_with_authors')
Authentication
Email/Password Auth
// Sign up
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'securepassword',
})
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'securepassword',
})
// Sign out
await supabase.auth.signOut()
// Get current user
const { data: { user } } = await supabase.auth.getUser()
OAuth Providers
// GitHub OAuth
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
}
})
// Google
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourapp.com/auth/callback',
}
})
// Available providers: github, google, facebook, twitter, apple, etc.
Magic Links
// Send magic link
const { data, error } = await supabase.auth.signInWithOtp({
email: '[email protected]',
})
Auth Helpers
// For React/Next.js
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
const supabase = createClientComponentClient()
// Auth state change listener
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') {
console.log('Signed in:', session.user)
}
})
Real-time
Subscribe to Changes
// Subscribe to table changes
const channel = supabase
.channel('posts')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts'
},
(payload) => {
console.log('New post:', payload.new)
}
)
.subscribe()
// Subscribe to specific row
const channel = supabase
.channel('posts')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'posts',
filter: 'id=eq.1'
},
(payload) => {
console.log('Post updated:', payload.new)
}
)
.subscribe()
// Unsubscribe
channel.unsubscribe()
Broadcast (Pub/Sub)
// Simple pub/sub between clients
const channel = supabase
.channel('chat-room')
.on('broadcast', { event: 'typing' }, ({ payload }) => {
console.log('User typing:', payload)
})
.subscribe()
// Send to channel
channel.send({
type: 'broadcast',
event: 'typing',
payload: { user: 'John' }
})
Storage
Bucket Management
// Create bucket
const { data, error } = await supabase.storage
.createBucket('avatars', {
public: true,
allowedMimeTypes: ['image/*'],
fileSizeLimit: '2MB',
})
// List buckets
const { data: buckets } = await supabase.storage.listBuckets()
File Operations
// Upload file
const { data, error } = await supabase.storage
.from('avatars')
.upload('folder/avatar.png', file)
// Upload with custom name
const { data, error } = await supabase.storage
.from('avatars')
.upload('avatar.png', file, {
cacheControl: '3600',
upsert: true,
})
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl('avatar.png')
// Download
const { data, error } = await supabase.storage
.from('avatars')
.download('avatar.png')
// Delete
const { error } = await supabase.storage
.from('avatars')
.remove(['avatar.png'])
Signed URLs
// Generate signed URL (temporary access)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('avatar.png', 60) // 60 seconds
console.log(data.signedUrl)
Edge Functions
Creating Functions
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/[email protected]/http/server.ts'
serve(async (req) => {
// CORS headers
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
},
})
}
// Get auth token
const authHeader = req.headers.get('Authorization')!
// Verify user
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_ANON_KEY')!,
{ global: { headers: { Authorization: authHeader } } }
)
const { data: { user } } = await supabase.auth.getUser()
return new Response(
JSON.stringify({
message: `Hello, ${user?.email || 'world'}!`,
timestamp: new Date().toISOString()
}),
{
headers: { 'Content-Type': 'application/json' },
}
)
})
Deploying
# Deploy function
supabase functions deploy hello-world
# Deploy with secrets
supabase functions deploy hello-world --no-verify-jwt
supabase functions secret set API_KEY=xxx
Calling from Client
// Call edge function
const { data, error } = await supabase.functions.invoke('hello-world', {
body: { name: 'John' }
})
Using with Frontend Frameworks
React
// Create client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
// In components
function App() {
const [posts, setPosts] = useState([])
useEffect(() => {
// Fetch posts
supabase
.from('posts')
.select('*')
.then(({ data }) => setPosts(data))
}, [])
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Next.js (App Router)
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export function createClient() {
const cookieStore = 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) {
cookieStore.set(name, value, options)
},
remove(name: string, options) {
cookieStore.delete(name, options)
},
},
}
)
}
Vue
// plugins/supabase.js
import { createClient } from '@supabase/supabase-js'
export default ({ app }, inject) => {
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY
)
app.config.globalProperties.$supabase = supabase
inject('supabase', supabase)
}
Database Extensions
PostGIS (Geographic)
-- Enable PostGIS
CREATE EXTENSION postgis;
-- Create location table
CREATE TABLE locations (
id BIGINT GENERATED BY DEFAULT ASID IDENTITY PRIMARY KEY,
name TEXT,
coordinates GEOGRAPHY(POINT, 4326)
);
-- Query nearby locations
SELECT * FROM locations
WHERE ST_DWithin(
coordinates,
ST_MakePoint(-122.4194, 37.7749)::geography,
5000 -- 5km radius
);
Vector (Embeddings)
-- Enable vector extension
CREATE EXTENSION vector;
-- Create embedding column
ALTER TABLE posts ADD COLUMN embedding vector(1536);
-- Query similar
SELECT * FROM posts
ORDER BY embedding <=> query_embedding
LIMIT 5;
Supabase vs Firebase
| Feature | Supabase | Firebase |
|---|---|---|
| Database | PostgreSQL | NoSQL |
| Query Language | SQL | NoSQL queries |
| Real-time | PostgreSQL logical decoding | Proprietary |
| Storage | S3-compatible | Google Cloud Storage |
| Auth | GoTrue + PostgreSQL | Proprietary |
| Open Source | โ | โ |
| Pricing | Free tier + pay | Free tier + pay |
| GraphQL | PostgREST + GraphQL | โ |
Conclusion
Supabase is an excellent choice when you need:
- PostgreSQL’s power with modern developer experience
- Open source - self-host or use managed
- SQL - full query capabilities
- Real-time - built-in subscriptions
- Scalability - from startup to enterprise
Perfect for: MVPs, startups, indie hackers, and teams that know SQL.
Comments