Tutorial

Firebase vs Supabase: Complete Setup Comparison [2025]

House of Loops TeamSeptember 5, 202510 min read
Firebase vs Supabase: Complete Setup Comparison [2025]

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

  1. Export Firestore data
  2. Transform to relational schema
  3. Import to PostgreSQL
  4. Update auth integration
  5. Rewrite security rules to RLS

Supabase to Firebase

  1. Export PostgreSQL data
  2. Flatten to document structure
  3. Import to Firestore
  4. Update auth integration
  5. 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

Supabase

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

Join House of Loops Today →

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!

H

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

Join 1,000+ automation enthusiasts