Firebase vs Supabase: Complete Setup Comparison [2025]
![Firebase vs Supabase: Complete Setup Comparison [2025]](/blog/images/firebase-vs-supabase.jpg)
Firebase vs Supabase: Complete Setup Comparison
Choosing a backend platform is one of the most important decisions in your project. Firebase and Supabase are both excellent "Backend as a Service" (BaaS) platforms, but they have fundamentally different approaches. In this comprehensive tutorial, we'll build the same application on both platforms so you can see the differences firsthand.
Quick Overview
Firebase
- Created by: Google (acquired in 2014)
- Database: NoSQL (Firestore) + Realtime Database
- Open Source: No (proprietary)
- Best for: Mobile apps, real-time features, Google ecosystem integration
Supabase
- Created by: Supabase Inc. (launched 2020)
- Database: PostgreSQL (relational)
- Open Source: Yes (MIT license)
- Best for: SQL-first projects, open-source preference, self-hosting
What We're Building
To compare both platforms fairly, we'll build the same app: a real-time chat application with:
- User authentication (email/password)
- Real-time message updates
- User presence (online/offline status)
- File uploads (profile pictures)
- Data security rules
This covers the core features you'll use in most applications.
Project Setup: Side-by-Side
Firebase Setup
# Install Firebase
npm install firebase
# Initialize Firebase in your project
npm install -g firebase-tools
firebase login
firebase init
Create firebase-config.js:
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
const firebaseConfig = {
apiKey: 'YOUR_API_KEY',
authDomain: 'your-app.firebaseapp.com',
projectId: 'your-project-id',
storageBucket: 'your-app.appspot.com',
messagingSenderId: '123456789',
appId: '1:123456789:web:abcdef',
};
const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
Supabase Setup
# Install Supabase
npm install @supabase/supabase-js
Create supabase-config.js:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://your-project.supabase.co';
const supabaseKey = 'your-anon-key';
export const supabase = createClient(supabaseUrl, supabaseKey);
Winner: Supabase - Simpler setup with just two configuration values vs Firebase's six.
Round 1: Authentication
Firebase Authentication
import { auth } from './firebase-config';
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from 'firebase/auth';
// Sign up
async function signUp(email, password) {
try {
const userCredential = await createUserWithEmailAndPassword(auth, email, password);
console.log('User created:', userCredential.user);
} catch (error) {
console.error('Error:', error.message);
}
}
// Sign in
async function signIn(email, password) {
try {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
console.log('User signed in:', userCredential.user);
} catch (error) {
console.error('Error:', error.message);
}
}
// Sign out
async function logOut() {
await signOut(auth);
}
// Listen to auth state
onAuthStateChanged(auth, user => {
if (user) {
console.log('User is signed in:', user.uid);
} else {
console.log('User is signed out');
}
});
Supabase Authentication
import { supabase } from './supabase-config';
// Sign up
async function signUp(email, password) {
const { data, error } = await supabase.auth.signUp({
email,
password,
});
if (error) {
console.error('Error:', error.message);
} else {
console.log('User created:', data.user);
}
}
// Sign in
async function signIn(email, password) {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
console.error('Error:', error.message);
} else {
console.log('User signed in:', data.user);
}
}
// Sign out
async function logOut() {
const { error } = await supabase.auth.signOut();
}
// Listen to auth state
supabase.auth.onAuthStateChange((event, session) => {
if (session) {
console.log('User is signed in:', session.user.id);
} else {
console.log('User is signed out');
}
});
Verdict: Tie - Both offer similar functionality with clean APIs. Firebase has more auth providers out of the box (Google, Facebook, etc.), while Supabase has built-in email confirmation flows.
Round 2: Database Structure
Firebase (Firestore)
Firestore is a NoSQL document database. Data is stored in collections and documents:
import { db } from './firebase-config';
import {
collection,
addDoc,
query,
where,
getDocs,
onSnapshot,
serverTimestamp,
} from 'firebase/firestore';
// Database structure (conceptual)
/*
users (collection)
├─ userId1 (document)
│ ├─ name: "John"
│ └─ email: "john@example.com"
└─ userId2 (document)
messages (collection)
├─ messageId1 (document)
│ ├─ text: "Hello"
│ ├─ userId: "userId1"
│ └─ timestamp: Timestamp
└─ messageId2 (document)
*/
// Create a message
async function createMessage(text, userId) {
await addDoc(collection(db, 'messages'), {
text,
userId,
timestamp: serverTimestamp(),
});
}
// Query messages
async function getMessages() {
const q = query(collection(db, 'messages'), where('userId', '==', 'userId1'));
const snapshot = await getDocs(q);
return snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
}
// Real-time listener
function listenToMessages(callback) {
const q = query(collection(db, 'messages'));
return onSnapshot(q, snapshot => {
const messages = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}));
callback(messages);
});
}
Supabase (PostgreSQL)
Supabase uses PostgreSQL, a relational database. First, create tables in the Supabase dashboard:
-- Create users table (extends auth.users)
CREATE TABLE public.profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
name TEXT,
avatar_url TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Create messages table
CREATE TABLE public.messages (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
user_id UUID REFERENCES auth.users,
created_at TIMESTAMP DEFAULT NOW()
);
-- Create index for performance
CREATE INDEX messages_created_at_idx ON messages(created_at DESC);
Then use the JavaScript client:
import { supabase } from './supabase-config';
// Create a message
async function createMessage(text, userId) {
const { data, error } = await supabase.from('messages').insert([{ text, user_id: userId }]);
if (error) console.error(error);
return data;
}
// Query messages
async function getMessages() {
const { data, error } = await supabase
.from('messages')
.select('*')
.order('created_at', { ascending: false })
.limit(50);
return data;
}
// Real-time listener
function listenToMessages(callback) {
const subscription = supabase
.channel('messages')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'messages',
},
payload => {
callback(payload.new);
}
)
.subscribe();
return subscription;
}
// Complex query with JOIN
async function getMessagesWithUsers() {
const { data } = await supabase
.from('messages')
.select(
`
*,
profiles (
name,
avatar_url
)
`
)
.order('created_at', { ascending: false });
return data;
}
Winner: Supabase - PostgreSQL gives you powerful relational queries, JOINs, and better data integrity. NoSQL is flexible but can lead to data duplication and complex querying.
Round 3: Real-time Capabilities
Firebase Real-time
import { onSnapshot, collection, query, orderBy } from 'firebase/firestore';
// Listen to messages in real-time
const unsubscribe = onSnapshot(
query(collection(db, 'messages'), orderBy('timestamp', 'desc')),
snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type === 'added') {
console.log('New message:', change.doc.data());
}
if (change.type === 'modified') {
console.log('Modified message:', change.doc.data());
}
if (change.type === 'removed') {
console.log('Removed message:', change.doc.data());
}
});
}
);
// Cleanup
unsubscribe();
Supabase Real-time
// Listen to INSERT events
const insertSubscription = supabase
.channel('messages-insert')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
},
payload => {
console.log('New message:', payload.new);
}
)
.subscribe();
// Listen to UPDATE events
const updateSubscription = supabase
.channel('messages-update')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'messages',
},
payload => {
console.log('Updated message:', payload.new);
}
)
.subscribe();
// Listen to all changes
const allChanges = supabase
.channel('all-messages')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'messages',
},
payload => {
console.log('Change:', payload);
}
)
.subscribe();
// Cleanup
insertSubscription.unsubscribe();
Winner: Firebase - Firebase was built for real-time from day one and has more mature real-time features. Supabase's real-time is excellent but newer.
Round 4: Security Rules
Firebase Security Rules
Rules are written in Firebase's custom language:
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read their own profile
match /users/{userId} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if request.auth != null && request.auth.uid == userId;
}
// Anyone authenticated can read messages
// Only the message author can delete
match /messages/{messageId} {
allow read: if request.auth != null;
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
allow delete: if request.auth != null
&& resource.data.userId == request.auth.uid;
}
}
}
Supabase Row Level Security (RLS)
Rules are written in PostgreSQL policies:
-- Enable RLS on tables
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Users can only read their own profile
CREATE POLICY "Users can view own profile"
ON profiles FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Anyone authenticated can read messages
CREATE POLICY "Anyone can view messages"
ON messages FOR SELECT
USING (auth.role() = 'authenticated');
-- Users can create messages as themselves
CREATE POLICY "Users can create messages"
ON messages FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can delete their own messages
CREATE POLICY "Users can delete own messages"
ON messages FOR DELETE
USING (auth.uid() = user_id);
-- Advanced: Moderators can delete any message
CREATE POLICY "Moderators can delete any message"
ON messages FOR DELETE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid()
AND role = 'moderator'
)
);
Winner: Supabase - PostgreSQL RLS is more powerful and uses standard SQL. Firebase's custom language requires learning new syntax.
Round 5: File Storage
Firebase Storage
import { storage } from './firebase-config';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
async function uploadProfilePicture(file, userId) {
const storageRef = ref(storage, `avatars/${userId}`);
// Upload file
const snapshot = await uploadBytes(storageRef, file);
console.log('Uploaded:', snapshot);
// Get download URL
const downloadURL = await getDownloadURL(storageRef);
return downloadURL;
}
// With metadata
async function uploadWithMetadata(file, userId) {
const metadata = {
contentType: file.type,
customMetadata: {
uploadedBy: userId,
},
};
const storageRef = ref(storage, `avatars/${userId}`);
const snapshot = await uploadBytes(storageRef, file, metadata);
return getDownloadURL(storageRef);
}
Firebase Storage Rules:
// storage.rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /avatars/{userId} {
allow read: if request.auth != null;
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 5MB limit
&& request.resource.contentType.matches('image/.*');
}
}
}
Supabase Storage
import { supabase } from './supabase-config';
async function uploadProfilePicture(file, userId) {
const fileExt = file.name.split('.').pop();
const fileName = `${userId}.${fileExt}`;
// Upload file
const { data, error } = await supabase.storage.from('avatars').upload(fileName, file, {
upsert: true,
});
if (error) throw error;
// Get public URL
const {
data: { publicUrl },
} = supabase.storage.from('avatars').getPublicUrl(fileName);
return publicUrl;
}
// With transformation (resize image)
async function uploadAndResize(file, userId) {
const fileName = `${userId}.jpg`;
const { data, error } = await supabase.storage.from('avatars').upload(fileName, file, {
upsert: true,
});
// Get resized image URL
const {
data: { publicUrl },
} = supabase.storage.from('avatars').getPublicUrl(fileName, {
transform: {
width: 200,
height: 200,
resize: 'cover',
},
});
return publicUrl;
}
Supabase Storage Policies:
-- Enable RLS on storage
CREATE POLICY "Anyone can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
CREATE POLICY "Users can upload own avatar"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
CREATE POLICY "Users can update own avatar"
ON storage.objects FOR UPDATE
USING (
bucket_id = 'avatars'
AND auth.uid()::text = (storage.foldername(name))[1]
);
Winner: Supabase - Built-in image transformations (resize, crop) without extra services. Firebase requires Cloud Functions or third-party services.
Round 6: Pricing Comparison
Firebase Pricing (Spark/Free Tier)
- Authentication: Unlimited
- Firestore: 1GB storage, 50K reads/day, 20K writes/day
- Storage: 5GB, 1GB/day downloads
- Hosting: 10GB/month
Blaze (Pay as you go):
- $0.18 per GB storage
- $0.06 per 100K reads
- $0.18 per 100K writes
- More expensive as you scale
Supabase Pricing (Free Tier)
- Authentication: Unlimited
- Database: 500MB, unlimited API requests
- Storage: 1GB
- Bandwidth: 2GB
- Real-time: Unlimited connections (fair use)
Pro ($25/month):
- 8GB database
- 100GB storage
- 250GB bandwidth
- Daily backups
Winner: Supabase - More generous free tier and predictable pricing. Firebase costs can balloon with heavy reads/writes.
Round 7: Developer Experience
Firebase Pros:
- Mature ecosystem (10+ years)
- Excellent documentation
- Large community
- Many tutorials and courses
- Great Firebase Console UI
- Tight Google Cloud integration
Firebase Cons:
- Vendor lock-in (proprietary)
- Complex pricing at scale
- NoSQL can be limiting
- Requires learning custom syntax for rules
Supabase Pros:
- Open source (self-hostable)
- Standard PostgreSQL (portable)
- Powerful SQL queries
- Built-in PostgreSQL features (views, functions, triggers)
- Auto-generated REST and GraphQL APIs
- One-click backups
Supabase Cons:
- Newer ecosystem (fewer tutorials)
- Smaller community
- Some features still in beta
- PostgreSQL learning curve for NoSQL developers
Use Case Recommendations
Choose Firebase if:
- Building mobile apps (especially with Flutter)
- Need extensive authentication providers
- Want battle-tested real-time features
- Already in Google Cloud ecosystem
- Building a chat app or collaborative tool
- Need FCM (Firebase Cloud Messaging) for push notifications
Choose Supabase if:
- Want to avoid vendor lock-in
- Need complex relational queries
- Prefer SQL over NoSQL
- Want self-hosting option
- Building with PostgreSQL features (triggers, functions)
- Need built-in image transformations
- Want predictable pricing
Migration Path
Firebase to Supabase
- Export Firestore data
- Transform to relational schema
- Import to PostgreSQL
- Update auth integration
- Rewrite security rules to RLS
Supabase to Firebase
- Export PostgreSQL data
- Flatten to document structure
- Import to Firestore
- Update auth integration
- Rewrite RLS to Firebase rules
Both are feasible but require planning.
Hybrid Approach
You can use both! Common patterns:
// Firebase for auth, Supabase for database
import { auth } from './firebase-config';
import { supabase } from './supabase-config';
// Firebase auth
const user = auth.currentUser;
// Use Firebase ID token with Supabase
const token = await user.getIdToken();
supabase.auth.setSession({
access_token: token,
refresh_token: '',
});
Final Verdict
There's no universal winner – it depends on your needs:
Firebase wins for:
- Real-time features
- Mobile apps
- Quick prototypes
- Google ecosystem
Supabase wins for:
- Complex data models
- Open source preference
- Predictable costs
- SQL-first teams
Getting Started Resources
Firebase
- Firebase Documentation
- Free tier: Good for MVPs
- Time to first app: ~30 minutes
Supabase
- Supabase Documentation
- Free tier: Generous for side projects
- Time to first app: ~20 minutes
Integration with Other Tools
Both platforms integrate well with modern tools:
- Build internal tools: Retool connects to both
- No-code frontends: Bubble works with both via APIs
- Automation: Make.com and n8n support both
Join the House of Loops Community
Want to master backend development? Join House of Loops for:
- Complete Firebase & Supabase starter templates
- $100K+ in startup credits (both Firebase and Supabase credits included!)
- Weekly workshops on backend architecture
- Community of 1,000+ developers
- Expert advice on choosing the right stack
Both Firebase and Supabase are excellent platforms. Firebase is the safe, mature choice backed by Google. Supabase is the exciting open-source alternative with modern features. Try both, build prototypes, and choose what feels right for your project. Happy building!
House of Loops Team
House of Loops is a technology-focused community for learning and implementing advanced automation workflows using n8n, Strapi, AI/LLM, and DevSecOps tools.
Join Our Community![Supabase for Beginners: Build Your First Backend in 30 Minutes [2025]](/blog/images/supabase-beginners.jpg)
![Scaling n8n for Production: Performance Optimization [2025]](/blog/images/scaling-n8n-performance.jpg)
![Retool Tutorial: Build Internal Tools Fast [2025]](/blog/images/retool-tutorial.jpg)