When I first started a side project in early 2025, Firebase felt like the perfect backend‑as‑a‑service. Its real‑time database, generous free tier, and client SDKs let me spin up a prototype in a single afternoon. Six months later, after the product grew past the hobby stage, I hit a wall: complex queries, security concerns, and vendor lock‑in started to bite. That’s when I decided to migrate to Supabase, an open‑source alternative built on PostgreSQL. The move didn’t just change the technology stack, it fundamentally altered how I think about data, security, and real‑time updates.
Real‑time Subscriptions: From Document Paths to SQL Queries
Firebase’s real‑time capabilities are centered around a hierarchical document store. You subscribe to a path, and any change under that node triggers an update. It’s simple, but the simplicity can become a curse when you need to filter or join data.
// Firebase: listen to all messages in a chat room
import { getDatabase, ref, onValue } from "firebase/database";
const db = getDatabase();
const messagesRef = ref(db, "rooms/room123/messages");
onValue(messagesRef, snapshot => {
const msgs = snapshot.val();
console.log("New messages:", msgs);
});
The code above gives you every message in the room, regardless of who sent it, its status, or any other attribute. If you need only unread messages, you have to either download everything and filter client‑side, or restructure your data to nest unread flags under a separate node, a pattern that quickly proliferates.
Supabase’s real‑time layer sits on top of PostgreSQL’s logical replication. You subscribe to a SQL query, not a static path. That means you can express filters, joins, and even aggregate functions right in the subscription.
// Supabase: listen only to unread messages for the current user
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
"https://xyz.supabase.co",
"public-anon-key"
);
const userId = "user_42";
const subscription = supabase
.channel("public:messages")
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "messages",
filter: `room_id=eq.room123 and read_by @> '{${userId}}' = false`
},
payload => {
console.log("New unread message:", payload.new);
}
)
.subscribe();
Notice three key differences:
- Query‑level filtering happens server‑side, so the client only receives relevant rows.
- Joins are possible you could subscribe to a view that combines messages with user preferences.
- No need for extra data structures the same table stores everything, and the subscription logic lives in the query.
In practice, this reduced the amount of data transferred to my mobile app by roughly 70 % and eliminated a whole layer of client‑side state management.
Open‑Source SQL Power vs. NoSQL Document Store
Firebase’s NoSQL model is great for schemaless data, but it forces you to think in terms of denormalized documents. When relationships become more than one‑to‑many, you start writing duplicated data or performing multiple round‑trips.
// Firebase: fetch a post and its author (two separate reads)
const postRef = ref(db, `posts/${postId}`);
onValue(postRef, snap => {
const post = snap.val();
const authorRef = ref(db, `users/${post.authorId}`);
onValue(authorRef, aSnap => {
const author = aSnap.val();
console.log({ post, author });
});
});
Supabase lets you keep a normalized schema, leverage indexes, and write complex queries with a single request.
// Supabase: fetch a post with its author in one go
const { data, error } = await supabase
.from("posts")
.select(`*, author:users(*)`)
.eq("id", postId)
.single();
if (error) throw error;
console.log(data);
Beyond convenience, PostgreSQL brings a mature ecosystem:
- Full‑text search via
tsvectorandGINindexes. - Transactional guarantees you can roll back a series of changes if any step fails.
- Stored procedures encapsulate business logic close to the data.
- Extensions PostGIS for geospatial queries,
pgcryptofor encryption, and many more.
Because Supabase is open source, I could also spin up a local instance for integration tests, run migrations with pg_dump, and even contribute a bug fix upstream. That level of control simply isn’t possible with Firebase’s closed‑source backend.
Row‑Level Security (RLS) vs. Firebase Security Rules
Firebase’s security model is expressed as a set of declarative rules that run on the server before each read or write. They are powerful, but they operate on the document hierarchy and often require duplicated logic for each path.
// Firebase security rule: only the owner can edit a document
{
"rules": {
"posts": {
"$postId": {
".write": "auth != null && auth.uid == data.ownerId"
}
}
}
}
When you need to enforce the same rule across multiple collections, you end up copying the snippet or creating helper functions that can become hard to audit.
Supabase’s Row‑Level Security lives inside PostgreSQL. You write policies that reference any column, join other tables, or call functions. The policies are attached to a table, and PostgreSQL evaluates them automatically for every query.
-- Enable RLS on the posts table
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Policy: users can only update their own posts
CREATE POLICY "owner_can_update" ON posts
FOR UPDATE
USING (auth.uid() = owner_id);
-- Policy: anyone can read public posts, owners can read drafts
CREATE POLICY "read_access" ON posts
FOR SELECT
USING (
is_public = true OR auth.uid() = owner_id
);
From the client side, the same JavaScript call works, and PostgreSQL guarantees that the policies are enforced for every query, even raw SQL executed in a function.
// Supabase: attempt to update a post – policy will reject if not owner
const { error } = await supabase
.from("posts")
.update({ title: "New title" })
.eq("id", postId);
if (error) console.error("Permission denied", error.message);
Benefits I observed:
- Single source of truth security lives next to the data, not in a separate JSON file.
- Fine‑grained control policies can reference joins, allowing “owner of the parent project can edit child tasks” without extra code.
- Auditable you can query
pg_policyto list all active policies, making compliance checks straightforward.
How the Workflow Shifted
Before (Firebase)
- Data modeling: think in nested JSON, often duplicate data to avoid joins.
- Real‑time: subscribe to broad paths, filter client‑side.
- Security: maintain a separate ruleset, duplicate logic across collections.
- Testing: rely on the Firebase emulator, which mimics the realtime DB but not Firestore’s exact semantics.
After (Supabase)
- Data modeling: design relational tables, let foreign keys enforce integrity.
- Real‑time: write a subscription query once, let the server push only what matches.
- Security: write SQL policies once per table, reuse functions for complex checks.
- Testing: spin up a Dockerized PostgreSQL instance, run migrations, and execute integration tests against the real engine.
The net effect was a 30 % reduction in client‑side boilerplate and a 50 % faster iteration cycle because I could reason about data relationships directly in SQL rather than stitching together document paths.
Migration Tips for Teams Considering the Switch
- Export your Firebase data: use the
firebase-toolsfirestore:exportcommand to get a JSON dump. - Map documents to tables: start with a simple one‑to‑many mapping, then refine with foreign keys.
- Leverage Supabase’s migration tooling: the
supabase db pushcommand lets you apply schema changes declaratively. - Replace security rules gradually: enable RLS on a table, write a permissive policy, then tighten it as you migrate client code.
- Monitor real‑time traffic: Supabase’s dashboard shows subscription counts; compare them to your Firebase usage to catch any over‑subscription.
Final Thoughts
Moving from Firebase to Supabase felt like swapping a Swiss‑army knife for a well‑organized toolbox. I still love Firebase’s instant‑setup vibe for prototypes, but once a product matures, the relational power, open‑source transparency, and robust security model of Supabase pay off in developer velocity and confidence.
If you’re wrestling with noisy client‑side state, duplicated security logic, or the limits of NoSQL queries, give Supabase a spin. The learning curve is modest especially if you already know SQL, and the payoff is a cleaner, more maintainable workflow that scales with your ambitions.