← All posts

7 min read

Firebase vs Supabase for Flutter Apps: How I Choose a Backend

Firebase vs Supabase for Flutter apps: documents vs Postgres, security rules vs RLS, offline support, pricing shapes and lock-in, plus when I pick each one.

  • Flutter
  • Backend
  • Firebase
  • Supabase
Cover illustration for Firebase vs Supabase for Flutter Apps: How I Choose a Backend

“Should we use Firebase or Supabase?” usually comes up before anyone has decided what the app stores. That’s backwards. The real question is what shape your data has. Once you know that, the backend mostly picks itself.

I built the backend for Range Buddy on Firebase, using it for authentication and real-time data sync, and it was the right call for that app. I’ve also spent plenty of time in plain SQL databases behind .NET APIs, which is why Supabase’s Postgres-first approach feels familiar to me. Both are good. They just make different promises.

The one-sentence version

Firebase gives you a document database, auth, functions, push and analytics that fit together tightly, with excellent mobile SDKs. Supabase gives you a real Postgres database with auth, storage, realtime and edge functions built around it, and an API generated from your schema.

Everything else follows from “documents” vs “tables.”

Data modeling: documents vs relational

Firestore stores documents in collections. Documents can hold nested maps and subcollections. There are no joins, so you design around the screens you need to render: if the order list shows the customer’s name, you copy the customer’s name into each order document. That’s denormalization, and in Firestore it’s normal, not a smell.

It works really well when your data is naturally hierarchical and read far more often than it’s written: a user’s profile, their sessions, the messages in a chat room. It gets painful when the data is really relational. Think “show every booking across all locations for this instructor, filtered by status, sorted by date, with the customer’s latest payment.” In Firestore that turns into duplicated fields, composite indexes and Cloud Functions that keep copies in sync.

Supabase is just Postgres. You get foreign keys, joins, constraints, views, transactions and SQL. The Flutter client lets you query tables directly, including related rows:

final supabase = Supabase.instance.client;

final bookings = await supabase
    .from('bookings')
    .select('id, starts_at, status, customer:customers(name, email)')
    .eq('instructor_id', instructorId)
    .order('starts_at');

The Firestore equivalent of a simple scoped query looks like this:

final stream = FirebaseFirestore.instance
    .collection('orders')
    .where('ownerId', isEqualTo: uid)
    .orderBy('createdAt', descending: true)
    .snapshots();

Both are pleasant to write. The difference shows up six months later, when the product owner asks for a report that cuts across five collections.

My rule of thumb: if I can sketch the data as a tree, Firestore is fine. If I sketch it and get a diagram full of lines between boxes, I want Postgres.

Security: rules vs Row Level Security

Both platforms let the client talk to the database directly, so access control lives in the backend instead of your Flutter code. That part is non-negotiable. Anything you check only in the app can be bypassed.

Firestore uses Security Rules, a small language of its own that’s evaluated on every request:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /orders/{orderId} {
      allow read: if request.auth != null
        && request.auth.uid == resource.data.ownerId;
    }
  }
}

Supabase uses Postgres Row Level Security. You enable it per table and write policies in SQL:

alter table orders enable row level security;

create policy "Users can read their own orders"
on orders for select
to authenticated
using ((select auth.uid()) = user_id);

The big gotcha with Supabase: a table without RLS enabled is readable through the public API by anyone with your anon key, and that key ships in your app. Supabase’s dashboard warns you about this, but I still check every table during review.

The big gotcha with Firestore: rules aren’t filters. If a query could return a document the user isn’t allowed to read, the whole query fails. Your queries have to match your rules, which surprises people the first time.

Both have emulators or local dev setups where you can test rules and policies. Use them. I treat access rules like any other code that needs tests, as I describe in my mobile app security checklist.

Offline support

This is where Firebase has a real advantage for mobile apps. Firestore’s SDK caches data locally and has offline persistence turned on by default on Android and iOS. Reads come from the cache when there’s no connection, writes are queued and synced later, and your snapshots() streams keep emitting. For many apps, that’s offline support you get for free.

Supabase’s Flutter client doesn’t give you an offline cache. If the app needs to work without a connection, you build that layer yourself: a local database such as Drift, a sync strategy and conflict handling, or a third-party sync tool designed to sit on top of Postgres. That’s absolutely doable, and I walk through the pattern in offline-first Flutter apps, but it’s real work and it belongs in the estimate.

If offline matters and the budget is tight, that alone can decide it.

Server-side logic

Sooner or later you need code that doesn’t run on the phone: sending emails, calling Stripe, processing webhooks, running scheduled jobs.

  • Firebase: Cloud Functions, triggered by HTTP calls, database changes, auth events or schedules. They’re mature and well integrated, and they require the pay-as-you-go plan.
  • Supabase: Edge Functions written in TypeScript on Deno, plus everything Postgres can do itself: triggers, database functions, scheduled jobs through extensions. A surprising amount of logic can live in SQL.

Push notifications are worth calling out. Firebase Cloud Messaging is the way to deliver push to Android, and it also handles iOS through APNs. Supabase doesn’t have its own push service, so a Supabase app usually still uses FCM, with an Edge Function or database webhook doing the sending. Using Firebase for push alongside a Supabase database is completely normal. More on that side in push notifications with FCM.

Pricing shapes (not numbers)

I won’t quote prices because they change, but the shape of each model matters more than the numbers anyway.

  • Firestore charges mainly per document read, write and delete, plus storage and network. Costs track how your app uses data. One badly designed screen that listens to a big collection can quietly multiply your reads.
  • Supabase sells plans with included quotas for database size, compute, storage, bandwidth and monthly active users, with charges for overages. Costs track how big your project is more than how chatty it is.

Neither is cheaper in general. A read-heavy social feed and a back-office tool with a few hundred users will come out very differently on each. Model your expected usage on both pricing pages before you commit, and set budget alerts either way.

Lock-in

Firestore is proprietary. Its data model, query language and rules don’t exist anywhere else, so leaving means rewriting your data layer and migrating the data into a different shape.

Supabase is open source and can be self-hosted, and the core is standard Postgres. You can pg_dump your database and take it to any Postgres host. That said, if you lean heavily on Supabase Auth, Realtime and Storage, moving still means replacing those pieces. Less lock-in isn’t the same as no lock-in.

Whichever you pick, putting a repository layer between your Flutter UI and the backend SDK keeps a future switch contained. That’s one of the reasons I care about a layered architecture.

When I pick each one

If the app… I lean toward
Needs solid offline support on a tight budget Firebase
Has hierarchical, read-heavy data (profiles, chats, feeds) Firebase
Has relational data, reporting or admin queries Supabase
Already has a team that knows SQL Supabase
Needs to avoid proprietary data formats Supabase
Leans on analytics, Crashlytics, Remote Config, App Check Firebase

And sometimes I pick neither. If the app has complex business rules, multiple client types or strict compliance needs, a custom API in front of a regular database is often the better long-term answer. I’ve written about building a .NET API for a Flutter app for exactly that case.

Takeaways

  • Decide based on the shape of your data: trees suit Firestore, relationships suit Postgres.
  • Access control must live in Security Rules or RLS. Never trust checks that only run in the app.
  • Firestore gives you offline caching out of the box. With Supabase, budget for building it.
  • Firestore cost follows usage patterns; Supabase cost follows project size and quotas.
  • Supabase is less locked in, not lock-free. A repository layer protects you either way.
  • Mixing is fine: plenty of Supabase apps still use FCM for push.

If you’re weighing backends for a Flutter app and want a second opinion based on your actual data, get in touch.

Comments

Questions, corrections or your own experience — leave a comment below (GitHub sign-in).