Lesson 53 of 77 intermediate

Firebase vs Supabase vs Custom Backend — Practical Comparison

Choosing the right backend for your Flutter app — tradeoffs, pricing, and migration paths

Open interactive version (quiz + challenge)

Real-world analogy

Firebase is a fully-furnished apartment — move in instantly but you can't knock down walls. Supabase is a furnished apartment where you own the building — more control but you manage the plumbing. A custom backend is building your own house — maximum control, maximum responsibility.

What is it?

Firebase, Supabase, and custom backends are three approaches to providing server-side functionality for Flutter apps, each with different tradeoffs in speed of development, flexibility, cost, and control.

Real-world relevance

A school management app starts with Firebase for rapid MVP (auth + Firestore for student records + FCM for notifications). At 50 schools with heavy read traffic, Firestore costs spike. The team migrates to Supabase — PostgreSQL handles complex attendance reports with SQL joins, RLS secures per-school data access, and costs become predictable.

Key points

Code example

// Supabase Flutter — auth + realtime + RLS example
import 'package:supabase_flutter/supabase_flutter.dart';

Future<void> main() async {
  await Supabase.initialize(
    url: 'https://your-project.supabase.co',
    anonKey: 'your-anon-key',
  );
  runApp(const App());
}

final supabase = Supabase.instance.client;

// Sign in with email
Future<void> signIn(String email, String password) async {
  final response = await supabase.auth.signInWithPassword(
    email: email,
    password: password,
  );
  // JWT is automatically attached to subsequent requests
}

// Query with RLS — only returns rows the authenticated user can access
Future<List<Map<String, dynamic>>> fetchMyWorkspaces() async {
  final data = await supabase
      .from('workspaces')
      .select('id, name, members(user_id, role)')
      .order('created_at', ascending: false);
  return data;
}

// Realtime subscription — listen to new messages in a channel
void subscribeToChannel(String channelId, Function(Map) onMessage) {
  supabase
      .channel('messages:$channelId')
      .onPostgresChanges(
        event: PostgresChangeEvent.insert,
        schema: 'public',
        table: 'messages',
        filter: PostgresChangeFilter(
          type: PostgresChangeFilterType.eq,
          column: 'channel_id',
          value: channelId,
        ),
        callback: (payload) => onMessage(payload.newRecord),
      )
      .subscribe();
}

Line-by-line walkthrough

  1. 1. Supabase.initialize() is called before runApp — it sets up the global client with your project URL and anon key (safe to include in client code).
  2. 2. supabase.auth.signInWithPassword returns an AuthResponse — the JWT session is stored automatically and appended to all subsequent API calls.
  3. 3. fetchMyWorkspaces uses Supabase's query builder — .select('id, name, members(user_id, role)') performs a JOIN to the members table in a single request.
  4. 4. RLS policies on the 'workspaces' table (defined in Supabase dashboard or SQL) automatically filter results — the Flutter client doesn't need to add userId filters.
  5. 5. supabase.channel() creates a named Realtime channel — the name is arbitrary but should be unique per subscription to avoid conflicts.
  6. 6. onPostgresChanges with PostgresChangeEvent.insert listens only to new row insertions, not updates or deletes — reducing noise.
  7. 7. The filter ensures only messages for the specific channelId trigger the callback — server-side filtering reduces bandwidth.
  8. 8. payload.newRecord is a Map of the newly inserted row's columns — typed models should be created with fromJson for production use.

Spot the bug

// Supabase realtime subscription
class ChatNotifier extends StateNotifier<List<Message>> {
  ChatNotifier() : super([]);
  RealtimeChannel? _channel;

  void subscribe(String channelId) {
    _channel = supabase
        .channel('messages')
        .onPostgresChanges(
          event: PostgresChangeEvent.insert,
          schema: 'public',
          table: 'messages',
          callback: (payload) {
            state = [...state, Message.fromJson(payload.newRecord)];
          },
        )
        .subscribe();
  }

  @override
  void dispose() {
    _channel?.unsubscribe();
    super.dispose();
  }
}
Need a hint?
Multiple users open different chat channels. Messages from other channels appear in the wrong chat. What's missing?
Show answer
Bug: the channel filter is missing — all message inserts across the entire 'messages' table trigger the callback regardless of channel_id. Fix: add a PostgresChangeFilter to the onPostgresChanges call: filter: PostgresChangeFilter(type: PostgresChangeFilterType.eq, column: 'channel_id', value: channelId). Also, the channel name 'messages' is shared across all ChatNotifier instances — if two channels are open simultaneously, they conflict. Fix: use a unique channel name like 'messages:$channelId' so each subscription is isolated.

Explain like I'm 5

Firebase is like a toy train set — it's ready to play with in 5 minutes and everything fits together perfectly. Supabase is like Lego — a bit more work to build but you can make any shape you want and you can take it apart and rebuild it. A custom backend is like woodworking — you make exactly what you want but you need real skills and tools.

Fun fact

Supabase reached 1 million databases hosted in 2023, just three years after launch — growing faster than Firebase did in its first three years, largely because of the PostgreSQL-familiar developer experience.

Hands-on challenge

Design the data layer for a school management app: (1) Define a Supabase schema for schools, students, and attendance with appropriate foreign keys. (2) Write RLS policies so teachers only see students from their school. (3) Implement a Supabase Realtime subscription in Flutter that updates the UI when a new attendance record is inserted. (4) Compare how you would implement the same offline-first requirement with Firebase vs Supabase.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery