What is Supabase
Supabase is a BaaS (Backend as a Service) platform gaining attention as an open-source Firebase alternative. It provides authentication, real-time, storage, and more features based on PostgreSQL.
flowchart TB
subgraph Client["Client Application"]
App["React, Next.js, Vue, Flutter, etc."]
end
subgraph Supabase["Supabase Platform"]
subgraph Services["Services Layer"]
Auth["Auth<br/>(GoTrue)"]
Realtime["Realtime<br/>(Elixir)"]
Storage["Storage<br/>(S3 compat.)"]
end
subgraph API["API Layer"]
Edge["Edge Functions<br/>(Deno)"]
PostgREST["PostgREST<br/>(Auto-generated API)"]
end
subgraph DB["Database Layer"]
Postgres["PostgreSQL<br/>(Row Level Security, Extensions)"]
end
end
Client --> Supabase
Services --> DB
API --> DB
Setup
# Install Supabase CLI
npm install -g supabase
# Start local development environment
supabase init
supabase start
# Install client library
npm install @supabase/supabase-js
Client Initialization
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
// Server-side (Service Role Key)
export const supabaseAdmin = createClient<Database>(
supabaseUrl,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
auth: {
autoRefreshToken: false,
persistSession: false,
},
}
);
Type Generation
# Generate types from database schema
supabase gen types typescript --local > lib/database.types.ts
// lib/database.types.ts (auto-generated)
export type Database = {
public: {
Tables: {
users: {
Row: {
id: string;
email: string;
name: string;
avatar_url: string | null;
created_at: string;
};
Insert: {
id?: string;
email: string;
name: string;
avatar_url?: string | null;
created_at?: string;
};
Update: {
id?: string;
email?: string;
name?: string;
avatar_url?: string | null;
created_at?: string;
};
};
posts: {
Row: {
id: number;
title: string;
content: string;
author_id: string;
published: boolean;
created_at: string;
};
Insert: Omit<Posts['Row'], 'id' | 'created_at'>;
Update: Partial<Posts['Insert']>;
};
};
};
};
Authentication
Email/Password Authentication
// Sign up
async function signUp(email: string, password: string, name: string) {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
name,
},
},
});
if (error) throw error;
return data;
}
// Login
async function signIn(email: string, password: string) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
return data;
}
// Logout
async function signOut() {
const { error } = await supabase.auth.signOut();
if (error) throw error;
}
// Get session
async function getSession() {
const { data: { session } } = await supabase.auth.getSession();
return session;
}
OAuth Authentication
// Google authentication
async function signInWithGoogle() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
});
if (error) throw error;
return data;
}
// GitHub authentication
async function signInWithGitHub() {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
scopes: 'read:user user:email',
},
});
if (error) throw error;
return data;
}
Watching Authentication State
// hooks/useAuth.ts
import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import type { User, Session } from '@supabase/supabase-js';
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Get initial session
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
});
// Watch authentication state changes
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(_event, session) => {
setSession(session);
setUser(session?.user ?? null);
}
);
return () => subscription.unsubscribe();
}, []);
return { user, session, loading };
}
Database Operations
CRUD Operations
// Fetch
async function getPosts() {
const { data, error } = await supabase
.from('posts')
.select('*, author:users(name, avatar_url)')
.eq('published', true)
.order('created_at', { ascending: false })
.limit(10);
if (error) throw error;
return data;
}
// Fetch single record
async function getPost(id: number) {
const { data, error } = await supabase
.from('posts')
.select('*, author:users(name, avatar_url), comments(*, author:users(name))')
.eq('id', id)
.single();
if (error) throw error;
return data;
}
// Create
async function createPost(post: { title: string; content: string }) {
const { data: { user } } = await supabase.auth.getUser();
const { data, error } = await supabase
.from('posts')
.insert({
title: post.title,
content: post.content,
author_id: user!.id,
})
.select()
.single();
if (error) throw error;
return data;
}
// Update
async function updatePost(id: number, updates: Partial<Post>) {
const { data, error } = await supabase
.from('posts')
.update(updates)
.eq('id', id)
.select()
.single();
if (error) throw error;
return data;
}
// Delete
async function deletePost(id: number) {
const { error } = await supabase
.from('posts')
.delete()
.eq('id', id);
if (error) throw error;
}
Filtering
// Complex conditions
const { data } = await supabase
.from('products')
.select('*')
.gte('price', 100)
.lte('price', 500)
.in('category', ['electronics', 'books'])
.ilike('name', '%phone%')
.not('deleted_at', 'is', null);
// OR conditions
const { data } = await supabase
.from('posts')
.select('*')
.or('status.eq.published,author_id.eq.123');
// Full-text search
const { data } = await supabase
.from('posts')
.select('*')
.textSearch('title', 'TypeScript React');
// Range queries
const { data } = await supabase
.from('events')
.select('*')
.gte('start_date', '2024-01-01')
.lte('end_date', '2024-12-31');
Row Level Security (RLS)
-- Enable RLS on table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: Published posts are viewable by everyone
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (published = true);
-- Policy: Users can view all their own posts
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
-- Policy: Authenticated users can create posts
CREATE POLICY "Authenticated users can create 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);
-- Policy: Users can only delete their own posts
CREATE POLICY "Users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
flowchart TB
Request["Request<br/>SELECT * FROM posts WHERE id = 1"]
RLS["RLS Policy Check<br/>published = true OR auth.uid() = author_id"]
Match["Policy match<br/>→ Return data"]
Fail["Policy fail<br/>→ Empty result"]
Request --> RLS
RLS -->|Pass| Match
RLS -->|Fail| Fail
Real-time
Real-time Subscriptions
// Watch table changes
const subscription = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{
event: '*', // INSERT, UPDATE, DELETE, or *
schema: 'public',
table: 'posts',
},
(payload) => {
console.log('Change received:', payload);
if (payload.eventType === 'INSERT') {
console.log('New post:', payload.new);
} else if (payload.eventType === 'UPDATE') {
console.log('Updated:', payload.old, '->', payload.new);
} else if (payload.eventType === 'DELETE') {
console.log('Deleted:', payload.old);
}
}
)
.subscribe();
// Filtered subscription
const subscription = supabase
.channel('user-posts')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: `author_id=eq.${userId}`,
},
(payload) => {
console.log('New post by user:', payload.new);
}
)
.subscribe();
// Cleanup
subscription.unsubscribe();
Presence (Online Status)
// Chat room presence
const room = supabase.channel('chat-room');
room
.on('presence', { event: 'sync' }, () => {
const state = room.presenceState();
console.log('Online users:', Object.keys(state));
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log('User joined:', key);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log('User left:', key);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await room.track({
user_id: userId,
online_at: new Date().toISOString(),
});
}
});
Storage
// File upload
async function uploadFile(file: File, path: string) {
const { data, error } = await supabase.storage
.from('avatars')
.upload(path, file, {
cacheControl: '3600',
upsert: true,
});
if (error) throw error;
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl(path);
return publicUrl;
}
// File download
async function downloadFile(path: string) {
const { data, error } = await supabase.storage
.from('documents')
.download(path);
if (error) throw error;
return data;
}
// Signed URL (with expiration)
async function getSignedUrl(path: string) {
const { data, error } = await supabase.storage
.from('private-files')
.createSignedUrl(path, 3600); // Valid for 1 hour
if (error) throw error;
return data.signedUrl;
}
// Delete file
async function deleteFile(path: string) {
const { error } = await supabase.storage
.from('avatars')
.remove([path]);
if (error) throw error;
}
// List files in folder
async function listFiles(folder: string) {
const { data, error } = await supabase.storage
.from('documents')
.list(folder, {
limit: 100,
sortBy: { column: 'created_at', order: 'desc' },
});
if (error) throw error;
return data;
}
Edge Functions
// supabase/functions/hello/index.ts
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 {
// Create Supabase client
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 } } = await supabaseClient.auth.getUser();
if (!user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { name } = await req.json();
return new Response(
JSON.stringify({ message: `Hello ${name}!`, userId: user.id }),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
});
// Calling from client
const { data, error } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
});
Next.js App Router Integration
// app/auth/callback/route.ts
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get('code');
if (code) {
const supabase = createRouteHandlerClient({ cookies });
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect(new URL('/', requestUrl.origin));
}
// 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();
// Check access to protected routes
if (req.nextUrl.pathname.startsWith('/dashboard') && !session) {
return NextResponse.redirect(new URL('/login', req.url));
}
return res;
}
export const config = {
matcher: ['/dashboard/:path*'],
};