Supabase is the open-source Firebase alternative that gives you a PostgreSQL database, authentication, real-time subscriptions, edge functions, and storage — all in one platform. In 2026, Supabase powers startups and enterprises alike with its developer-friendly DX and powerful PostgreSQL foundation. This guide covers everything from project setup to production.
📋 Table of Contents
What Supabase Provides
- PostgreSQL database — full Postgres with direct SQL access
- Auto-generated REST API — PostgREST turns your tables into REST endpoints
- Real-time subscriptions — listen to table changes via WebSocket
- Authentication — email, OAuth (Google, GitHub, Apple), phone OTP
- Storage — file uploads with CDN and row-level access policies
- Edge Functions — Deno-based serverless functions at the edge
- Vector search — pgvector extension for AI embeddings
Setup
npm install @supabase/supabase-js
# Or create project at supabase.com (free tier: 500MB DB, 1GB storage)
# Environment variables
SUPABASE_URL=https://xyzcompany.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs... # public, safe for frontend
SUPABASE_SERVICE_ROLE_KEY=eyJ... # private, server only!
Database Operations
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
// SELECT
const { data: users, error } = await supabase
.from('users')
.select('id, name, email, created_at')
.eq('active', true)
.order('created_at', { ascending: false })
.limit(10);
// SELECT with relations (foreign table)
const { data: posts } = await supabase
.from('posts')
.select(`
id,
title,
content,
users(name, email),
categories(name)
`)
.eq('published', true);
// INSERT
const { data, error } = await supabase
.from('posts')
.insert({
title: 'My Post',
content: 'Content here',
author_id: userId,
published: false,
})
.select()
.single();
// UPDATE
const { data: updated } = await supabase
.from('posts')
.update({ published: true })
.eq('id', postId)
.select()
.single();
// DELETE (soft delete via RLS is better)
await supabase.from('posts').delete().eq('id', postId);
// UPSERT
await supabase
.from('user_settings')
.upsert({ user_id: userId, theme: 'dark', language: 'en' })
.onConflict('user_id');
Row Level Security (RLS)
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users can read all published posts
CREATE POLICY "Anyone can read published posts"
ON posts FOR SELECT
USING (published = true);
-- Users can only see their own draft posts
CREATE POLICY "Users see own drafts"
ON posts FOR SELECT
USING (author_id = auth.uid() AND published = false);
-- Users can only insert/update their own posts
CREATE POLICY "Users manage own posts"
ON posts FOR ALL
USING (author_id = auth.uid());
-- Admin bypass (service role key bypasses RLS)
-- Use service role key in server-side code for admin operations
Authentication
// Sign up
const { data, error } = await supabase.auth.signUp({
email: 'alice@example.com',
password: 'securepassword',
options: {
data: { full_name: 'Alice Chen' } // stored in user metadata
}
});
// Sign in
const { data: session } = await supabase.auth.signInWithPassword({
email: 'alice@example.com',
password: 'securepassword'
});
// OAuth (Google, GitHub, etc.)
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`
}
});
// Get current user
const { data: { user } } = await supabase.auth.getUser();
// Listen to auth state changes
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') setUser(session?.user ?? null);
if (event === 'SIGNED_OUT') setUser(null);
});
// Sign out
await supabase.auth.signOut();
// Server-side auth (Next.js)
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export function createServerSupabase() {
const cookieStore = cookies();
return createServerClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!,
{ cookies: { get: (name) => cookieStore.get(name)?.value } }
);
}
Real-Time Subscriptions
// Subscribe to table changes
const subscription = supabase
.channel('posts-changes')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'posts' },
(payload) => {
console.log('Change received:', payload.eventType, payload.new);
if (payload.eventType === 'INSERT') addPost(payload.new);
if (payload.eventType === 'UPDATE') updatePost(payload.new);
if (payload.eventType === 'DELETE') removePost(payload.old);
}
)
.subscribe();
// Cleanup
subscription.unsubscribe();
// Broadcast (custom events between clients)
const channel = supabase.channel('room-1');
channel
.on('broadcast', { event: 'cursor-pos' }, ({ payload }) => {
updateCursor(payload.userId, payload.x, payload.y);
})
.subscribe();
// Send broadcast
await channel.send({
type: 'broadcast',
event: 'cursor-pos',
payload: { userId, x: mouseX, y: mouseY }
});
Edge Functions
// supabase/functions/send-email/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req: Request) => {
const { userId, subject, body } = await req.json();
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // admin access
);
const { data: user } = await supabase
.from('users')
.select('email')
.eq('id', userId)
.single();
// Send email via Resend
await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
Authorization: `Bearer ${Deno.env.get('RESEND_API_KEY')}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'app@myapp.com',
to: user.email,
subject,
html: body,
}),
});
return new Response(JSON.stringify({ success: true }), {
headers: { 'Content-Type': 'application/json' },
});
});
Vector Search (AI/Semantic)
-- Enable pgvector
CREATE EXTENSION IF NOT EXISTS vector;
-- Add embedding column
ALTER TABLE articles ADD COLUMN embedding vector(384);
-- Create vector index
CREATE INDEX ON articles USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Semantic search function
CREATE OR REPLACE FUNCTION semantic_search(query_embedding vector(384), match_count int = 10)
RETURNS TABLE(id bigint, title text, similarity float)
LANGUAGE plpgsql AS $$
BEGIN
RETURN QUERY
SELECT
articles.id,
articles.title,
1 - (articles.embedding <=> query_embedding) AS similarity
FROM articles
WHERE articles.embedding IS NOT NULL
ORDER BY articles.embedding <=> query_embedding
LIMIT match_count;
END;
$$;
// Call semantic search
const { data: results } = await supabase.rpc('semantic_search', {
query_embedding: await generateEmbedding(searchQuery),
match_count: 10
});
Supabase in 2026 is the fastest way to build a production backend. You get PostgreSQL power, real-time subscriptions, auth, and edge functions with a developer experience better than Firebase — and the data is actually yours. Start with the free tier (unlimited API calls, 500MB database), migrate to Pro ($25/month) when you need more resources.
📚 You might also like
🔗 Share this article




✍️ Leave a Comment