Should your AI-built backend leave Firebase?
Decide where an AI-built backend should move by comparing Firebase, Supabase, and managed Postgres across data, auth, workflows, and operations.

An application should leave Firebase when its data model, authorization rules, or server workflows have become harder to reason about than the product itself. It should not leave merely because the team has heard that Postgres is more serious. A rushed rewrite can replace one set of constraints with an outage, lost accounts, and months of split-brain behavior.
For most teams that have outgrown Firestore, Supabase is the practical middle route: relational data, SQL, managed authentication, storage, functions, and a generated API in one service. A plain managed Postgres service gives experienced teams more control and fewer platform conventions, but it also leaves them responsible for assembling authentication, APIs, file storage, background work, observability, and deployment. Staying on Firebase can still be correct when the document model fits and the team can repair its rules and functions without fighting the platform.
The destination follows the constraint you cannot keep paying for
Choose the destination by naming the constraint that is already costing engineering time. If nobody can state that constraint in one sentence, the team is not ready to migrate. A vague complaint such as "Firebase does not scale" is not a diagnosis. Firestore can handle large workloads; the more useful question is whether its access patterns and consistency model still fit this particular application.
Firebase remains a good fit when clients mostly read and write independent documents, offline behavior matters, real-time listeners are central, and Security Rules can express access without duplicating business state. The migration case grows stronger when one user action must update several related records, reports need joins across collections, referential integrity belongs in the database, or Cloud Functions have become the only place that anyone understands the real rules.
Supabase fits teams that want Postgres semantics without constructing every surrounding service. It reduces the number of decisions during a move, which matters when the current code was assembled by an AI builder and nobody has a trustworthy map of it. Its conventions also create boundaries: Supabase Auth has its own lifecycle, the generated data API depends on row-level security, and platform functions are not identical to a general application server.
Managed Postgres fits when the team already knows which web framework, identity provider, job system, storage service, and monitoring stack it wants. The database is portable, but the backend is something the team must design and operate. That is freedom only when someone owns those decisions.
Use a simple scorecard before discussing vendors. Give each statement one point for the destination it favors:
| Observed constraint | Stay on Firebase | Move to Supabase | Move to managed Postgres |
|---|---|---|---|
| Document reads and live listeners dominate | 2 | 1 | 0 |
| Joins and transactions define core flows | 0 | 2 | 2 |
| A small team wants auth and APIs together | 1 | 2 | 0 |
| The team already runs backend services | 0 | 1 | 2 |
| Database portability matters most | 0 | 1 | 2 |
| Offline clients must keep current behavior | 2 | 1 | 0 |
The score does not make the decision. It exposes the argument. Any row that nobody can score with evidence becomes a discovery task before migration begins.
Migration effort hides in behavior, not record count
A million simple documents may be easier to move than ten thousand documents whose meaning depends on triggers, client timestamps, denormalized counters, and Security Rules. Record count affects transfer time. Behavioral coupling determines the project.
Start by mapping every path that can change persistent state. In an AI-generated codebase, do not assume the obvious database client is the only writer. Browser code may write Firestore directly, server routes may use the Admin SDK, scheduled functions may repair counters, webhook handlers may update billing state, and deployment scripts may seed documents. Search for SDK initialization, collection names, callable functions, storage bucket calls, and environment variables. Then compare that map with production logs and cloud configuration. Code search alone misses deployed functions that no longer exist in the repository.
Classify each write by the guarantee it needs. A profile edit usually tolerates a straightforward update. An order confirmation may need a transaction, idempotency, an immutable event, and a retry policy. A counter maintained by a Firestore trigger may become a SQL aggregate, a database trigger, or an asynchronous projection. Those three designs fail differently. Copying the trigger line for line into a new function preserves an old workaround after the database has removed the original limitation.
Estimate work in four buckets: data transformation, identity cutover, workflow replacement, and client integration. Data transformation includes flattening nested documents, promoting repeated values into tables, choosing types, and resolving orphan references. Identity cutover includes password hashes, provider links, sessions, email templates, redirect URLs, and account recovery. Workflow replacement covers functions, queues, scheduled jobs, webhooks, and storage events. Client integration covers query changes, loading states, offline assumptions, error handling, and authorization checks.
One inventory file makes the estimate reviewable. It can be plain JSON stored beside the migration code:
{
"source": "firestore",
"collections": {
"orders": {
"writers": ["web-checkout", "payment-webhook"],
"readers": ["account-page", "support-console"],
"rules": ["owner-read", "support-read"],
"side_effects": ["send-receipt", "increment-customer-total"]
}
}
}
The artifact prevents a familiar failure: the team migrates visible screens, declares the database complete, and discovers later that an old webhook still writes only to Firestore. Add every writer before assigning dates. If a collection has an unknown writer or an unexplained field, discovery is incomplete.
Firestore export is a source dump, not a Postgres schema
Cloud Firestore's managed export produces LevelDB log files plus metadata in Cloud Storage. Firebase documents describe it as an export that can be imported into another Firestore database or loaded into BigQuery under specific conditions. It is not a relational dump that pg_restore can consume, and an export is not an exact snapshot taken at the moment the operation starts. Treat it as source material for a controlled transformation.
The migration needs an explicit mapping from document paths to tables. A document such as customers/{customerId}/orders/{orderId} contains a relationship in its path. In Postgres, that relationship usually belongs in an orders.customer_id foreign key. Arrays of primitive tags may stay arrays. Arrays of objects that change independently usually become child rows. Map fields may become typed columns when the product queries them, or jsonb when their shape is genuinely open. Choosing jsonb for every document makes the first import easy and keeps the old data model's weaknesses.
Firestore permits fields to be absent and allows type variation across documents. Postgres asks for a type and lets the schema enforce it. Profile the source before finalizing tables: count missing fields, enumerate observed types, find duplicate natural identifiers, and identify references to deleted documents. Decide how each anomaly will be handled. Silent coercion, such as turning an invalid timestamp into null, makes the import pass while moving a latent bug into production.
For Supabase, the community Firebase migration tools can turn Firestore collections into JSON and import transformed data, but they do not understand the application's intended relationships. For managed Postgres, a custom extractor and loader is often clearer. In both cases, make the transformation deterministic: the same source record should always produce the same target row and identifier. Store a source ID column until reconciliation is complete.
A useful target schema enforces the claims the application already makes. This fragment gives orders stable ownership and prevents repeated webhook deliveries from creating duplicates:
create table customers (
id uuid primary key,
firebase_uid text unique,
email text not null
);
create table orders (
id uuid primary key,
customer_id uuid not null references customers(id),
provider_event_id text not null unique,
status text not null check (status in ('pending', 'paid', 'cancelled')),
created_at timestamptz not null
);
After loading, reconcile facts rather than trusting a successful exit code. Compare counts by business category, sums of money in the smallest currency unit, minimum and maximum timestamps, distinct owner counts, and a sample of source-to-target records. Keep the queries in version control. A migration that cannot be rerun and checked is a one-time improvisation, not a release process.
Authentication moves separately from application data
User accounts are not ordinary rows because a valid account includes credentials, provider identities, verification state, sessions, recovery behavior, and references from application data. Migrating a users collection does not migrate Firebase Authentication, even when both use the same UID.
Firebase CLI supports auth:export, and Firebase exposes the password hash parameters needed for compatible imports. Supabase's Firebase Auth migration guide uses a two-part tool: it exports Firebase users to JSON and imports them into the destination auth.users table. The guide also tells operators to preserve the Firebase SCRYPT parameters, including the signer key, salt separator, rounds, and memory cost. That detail is the difference between keeping password users signed in after a normal login and forcing everyone through recovery.
Do not assume every identity transfers cleanly. Inventory password accounts, email-link accounts, phone users, anonymous users, and each OAuth provider. Check whether one person has linked providers under a single UID. Verify whether the destination preserves provider subject identifiers and how it handles duplicate email addresses. Custom claims need an explicit home, perhaps authorization tables or signed token claims. Sessions normally cannot be carried between unrelated auth systems, so plan for token expiry, reauthentication, and a clear user message.
There are three defensible cutover patterns. A forced reset is simplest but creates support load and should be a product decision, not an engineering surprise. A bulk hash import preserves password verification when the destination supports the source algorithm and parameters. A lazy migration validates the old credential on a user's first login, creates or updates the destination identity, and then stops consulting the old service for that user. Lazy migration reduces the day-one blast radius but prolongs the period when two identity systems can grant access.
Whichever pattern you choose, create an immutable identity mapping before moving business rows:
firebase_uid postgres_user_id
n7Yx... 2d99150e-3b33-4afb-9c5b-7cbd20b91a4d
Never join records by email during the migration. People change email addresses, providers may differ on normalization, and duplicates often exist after years of imports. Use the source UID as the migration identity, then attach the destination ID. Test disabled users, unverified users, deleted users with lingering data, provider-only users, and account recovery. The happy-path password login proves very little.
Authorization must be rewritten, not translated
Firestore Security Rules and Postgres row-level security answer similar questions through different execution models. A mechanical translation is dangerous. Firestore Rules evaluate a request against document paths and proposed data. Postgres policies filter rows for SQL operations, while database roles and grants decide which objects a caller may access. Server code with privileged credentials can bypass either system, so the trust boundary matters as much as the policy syntax.
First write an access matrix in product language. For each resource, state who may select, insert, update, and delete it, plus which fields or state transitions are allowed. "Users can update their profile" is incomplete if the same row contains an is_admin flag. "Owners can update orders" is wrong if customers may edit status from pending to paid.
A Supabase policy for selecting one's own orders might look like this:
alter table orders enable row level security;
create policy "customers read own orders"
on orders for select
to authenticated
using (customer_id = auth.uid());
That policy only works if orders.customer_id stores the same UUID returned by auth.uid(). If the imported row contains a Firebase UID string while the new token contains a Supabase UUID, the policy returns no rows. If a developer disables row-level security to make the screen work, it may return every row. The identity mapping and schema design therefore belong in the authorization review.
Plain managed Postgres does not provide auth.uid() unless the application creates an equivalent session context or always filters through trusted server code. Many teams choose the latter: clients call an API, the API verifies identity, and SQL receives the authenticated user ID as a parameter. This model is easier to inspect in one codebase, but every endpoint must apply authorization. Direct database access from a browser demands tighter role design and policy tests.
Test denial, not just success. For each role, attempt to read another tenant's row, change an ownership column, update a protected state, insert a foreign owner ID, and invoke the operation through every public route. Run these tests with the same public credentials and token claims that the client uses. An admin console query proves nothing about client isolation.
Custom workflows decide whether Supabase is enough
Supabase is usually enough when application behavior can live in SQL constraints, database functions, edge functions, scheduled jobs, webhooks, and a modest number of background tasks. Managed Postgres becomes the clearer choice when the product needs long-running workers, specialized runtimes, complex queues, private networking, or deployment control that would fight a platform function model.
List current Cloud Functions by trigger type, not file name. HTTP functions are APIs. Callable functions tie the client to Firebase's invocation protocol. Firestore triggers react to data changes. Authentication hooks react to identity events. Storage triggers process files. Scheduled functions run maintenance. Each category needs a destination with matching delivery and retry behavior.
Pay special attention to at-least-once delivery. A payment webhook, storage event, or queue message may arrive twice even if it usually does not. The new handler should claim a provider event ID in the same transaction as its state change. The unique constraint in the earlier orders schema turns the second delivery into a detectable duplicate. An in-memory flag or a log line does not provide that guarantee across instances.
Do not put every workflow into a database trigger just because Postgres can run it. Triggers work well for local invariants and small derived changes that must share a transaction. They are poor places to call third-party APIs, send email, or perform slow media processing. Those actions need a durable job record and a worker that retries with idempotency. The database should commit the business change and enqueue the intent together.
The popular recommendation to rewrite everything as SQL is appealing because it removes application code. It is wrong when it hides business workflows in functions that the rest of the team cannot deploy, trace, or test. Put consistency rules near the data. Put orchestration in a service with logs, timeouts, retries, and an owner. Supabase can host a small version of that service; a managed Postgres design expects the team to choose and run it elsewhere.
Operating burden is the work left after launch
The operating difference is not "managed versus unmanaged." Firebase, Supabase, and managed Postgres all manage infrastructure at some layer. The useful comparison is which failures remain yours and whether the team can detect and repair them.
Firebase removes most database administration, but the team still owns Security Rules, indexes, quotas, function deployments, cost surprises, regional choices, and application-level recovery. Supabase manages Postgres and bundles several backend services, but the team owns schema migrations, row policies, connection behavior, query performance, extension compatibility, and the interaction between platform components. A managed Postgres provider handles the database process, backups, and some maintenance while the team owns the API layer and every supporting service it selected.
Ask who will perform five recurring jobs: apply a schema change without breaking old clients, restore data to a known point, respond to a leaked server credential, diagnose a slow request across service boundaries, and rotate an authentication secret. If the answer is one founder who has never seen the deployment configuration, managed Postgres plus a custom service stack is too large a first move. If an experienced backend team already operates those controls, platform conventions may create more friction than help.
Cost comparisons need the same workload and failure assumptions. Firestore prices operations and storage according to its model. Postgres plans tend to bundle compute and then constrain connections, memory, storage, or throughput. A query that reads one relational aggregate is not comparable to a client fetching hundreds of documents, and a Postgres instance sized for peak load is not comparable to serverless idle cost. Replay production-shaped queries against a staging target and record latency, rows touched, connection use, and the work required to tune them. Do not extrapolate from an empty demo.
Portability also has layers. SQL tables move more easily between Postgres providers than Firestore data moves into a relational schema. Supabase-specific auth tables, storage metadata, generated APIs, policies, and edge functions still create migration work. That is acceptable when the services save more work today than they may cost later. Calling any backend "no lock-in" conceals the application dependencies that matter.
A safe cutover keeps one authority for every write
The safest migration separates copying data from changing authority. During the copy, Firestore remains authoritative. During cutover, each business entity must have one authoritative writer. Dual-writing for weeks without reconciliation creates two plausible versions of the truth and makes rollback harder, not easier.
Use this release sequence as a baseline:
- Freeze schema changes and inventory every reader, writer, function, rule, index, and secret.
- Build the target schema, identity mapping, authorization tests, and deterministic import in an isolated environment.
- Run a full rehearsal from a fresh source export, record duration, and reconcile business facts.
- Copy the latest data, capture changes that occurred after the copy began, then pause source writes for the final delta.
- Switch server writers first, then clients, monitor both systems, and keep a tested route back until target writes make rollback unsafe.
A low-downtime plan can use change capture or temporary dual writes, but it needs a conflict rule and a reconciliation ledger. Record the source operation ID, target transaction ID, entity ID, result, and retry state. Decide in advance whether the source or target wins if both change. If nobody can explain how a failed second write is repaired, the system is not dual-writing safely.
Keep compatibility at the API boundary where possible. If clients currently call an application API, change the API's storage implementation and preserve the contract. If clients talk directly to Firestore, introduce a repository layer or new API before moving every screen. This extra seam looks slower during week one and saves repeated edits across the rest of the release.
Define rollback as a tested procedure with a deadline. Before target writes begin, rollback may mean pointing clients back and discarding the target. After unique target-only records exist, rollback requires reverse transformation or a write freeze. State the exact event that closes the easy rollback window. A backup is useful, but it is not a rollback plan unless the team has restored it and measured the time.
The codebase may need repair before the backend moves
A migration amplifies whatever the current application has failed to make explicit. Generated projects often mix browser and server credentials, duplicate database calls across components, trust client-supplied owner IDs, and hide business rules inside UI event handlers. Moving those calls to Supabase or Postgres without changing the boundaries transfers the defects.
Run the migration only after answering four code questions. Which modules may access the database? Where does server-side identity become a trusted user ID? Which service owns each state transition? Which environment can read privileged secrets? If answers change by screen, establish a small data-access boundary first. It does not need to be elegant. It needs to make all writes discoverable and testable.
Secret handling deserves its own check. Firebase web configuration is designed to identify a project and is protected by Security Rules and service configuration, while Admin SDK credentials grant privileged access. Supabase has public client credentials intended to work with row policies and privileged server credentials that bypass normal restrictions. Managed Postgres connection strings normally belong on a server, never in browser code. Automated replacements often confuse these categories because all of them look like environment variables.
FixMyMess can audit an inherited AI application, repair its authentication and logic, harden security, refactor the code, and prepare deployment before or during a backend move. The useful outcome is not a vendor switch; it is a system whose data ownership, privileged paths, and release procedure a human can explain.
Do not migrate a codebase that cannot pass a basic write-path audit. Stabilize identity and business invariants, choose the smallest destination that supports them, and rehearse until reconciliation is boring. Firebase, Supabase, and managed Postgres can all run a sound product. None of them can supply the missing model of how that product is supposed to behave.
FAQ
When should an app move away from Firebase?
Move when relational queries, multi-record transactions, authorization, or server workflows consistently fight the document model. Do not move because of a generic claim that Firebase cannot scale; prove the mismatch with current access patterns and maintenance work.
Is Supabase easier to migrate to than managed Postgres?
Usually, because Supabase supplies Postgres, authentication, storage, generated APIs, and functions under one set of conventions. Managed Postgres leaves more architecture choices to the team, which is useful only when someone can own them.
Can Firestore data be imported directly into Postgres?
No. Firestore's managed export is not a pg_dump archive, so the team must map document paths and fields into tables, transform records, and reconcile the result. Treat a successful file transfer as the beginning of verification, not the end.
Can users keep their Firebase passwords after migration?
Sometimes. Firebase can export users and password hash parameters, and a compatible destination may import them, but provider accounts, linked identities, sessions, disabled users, and recovery flows still need separate tests.
Does Supabase remove Firebase vendor lock-in?
It makes the core relational data easier to move between Postgres systems, but the application can still depend on Supabase Auth, storage metadata, policies, generated APIs, and functions. Portability improves by degree; it does not become automatic.
Should a team dual-write to Firebase and Postgres?
Only for a short, controlled cutover with idempotency, a conflict rule, and a reconciliation ledger. Untracked dual writes create two authorities and make failures difficult to repair.
How much downtime does a Firebase migration require?
It depends on data volume, write rate, and whether the team can capture changes after the main copy. A rehearsed final delta and brief write pause are often safer than an elaborate low-downtime design nobody has tested.
Can Firestore Security Rules be converted to Postgres policies?
They must be redesigned around the new identity and access model, not translated line by line. Build an operation matrix, implement policies or server checks, and test that one user cannot read or change another user's rows.
Is managed Postgres cheaper than Firebase?
There is no honest answer without replaying the same workload. The services charge for different resources, and the staffing cost of APIs, workers, monitoring, recovery, and tuning can outweigh a lower database bill.
What should be migrated first from Firebase?
Start with an inventory and a stable identity mapping, then build the target schema and denial tests before moving production data. The first production cutover should be a bounded workflow whose writers and rollback path are fully known.