Every backend-as-a-service comparison is written for day one. Here is how fast you can ship a signup form, here is realtime with three lines, here is a file upload. All of that is true and none of it is the decision.
The decision is day nine hundred. Your product now needs a query the generated API cannot express, a background job that runs for eleven minutes, an authorization rule that depends on a record in a different table and a value from a third-party service, and a compliance requirement that the platform’s tiering does not cover. The question is no longer “how fast can I ship” but “what does it cost me to stop using this”, and by then the answer is fixed by decisions you made without noticing.
So this guide is built around the escape hatches. Not whether you will outgrow the abstraction, because if the product succeeds you will, but what happens at that moment: whether you can take your data, whether your users come with you, whether you can run the code the platform did not anticipate, and whether the thing underneath is a standard system or a proprietary one.
That framing changes the ranking. A platform with a mediocre developer experience and a clean exit is frequently the better purchase than a delightful one with none, because the delightful part is a few weeks of velocity and the exit is a quarter of migration nobody budgeted for.
None of this is an argument against using a BaaS. The argument for them is strong and gets stronger the smaller your team is: authentication, a database with an API, file storage, and a place to run functions is four infrastructure projects you did not do. It is an argument for choosing on exit cost rather than on onboarding speed.
Key takeaways
- The escape hatch that matters most is the boring one: can you take your data out in a format that runs somewhere else without a rewrite. Standard engines win this decisively over proprietary stores.
- Your users are the hardest thing to move. If you cannot export password hashes in a form another system can verify, migrating means forcing every user to reset their password, and a share of them simply will not.
- Authorization is the abstraction you outgrow first, well before storage or compute. Declarative row-level rules stop being expressible long before your data volume becomes interesting.
- Self-hostable is not the same as self-hosted parity. Check whether the open-source build is the same system the cloud runs, or a subset that is missing the pieces you depend on.
What a BaaS actually replaces, and what it does not
The category bundles four things that would otherwise be separate decisions: an authentication system with session handling and social providers, a datastore with an automatically generated API and a client-side access model, file storage with access control, and a place to run server-side functions. Most add realtime subscriptions, because once you own the client SDK and the datastore, pushing changes is cheap for you to implement and expensive for the customer to build.
What that combination genuinely removes is real. You are not standing up an identity provider, not writing a REST or GraphQL tier over your tables, not configuring object storage with signed URLs, and not operating a function runtime. For a small team that is months.
What it does not replace is worth naming, because these are the gaps teams hit:
- Durable background work. Function runtimes have execution time limits and no built-in orchestration. Anything long-running, multi-step or requiring retries across steps needs a queue and a worker you operate.
- Authorization beyond the row. Declarative rules express “this user owns this row” well and “this user’s manager’s team can read this row if the document is in review state” badly.
- Cross-service transactions. You get database transactions inside the datastore and nothing across the datastore, storage and a third-party API.
- Schema change management at scale. A migration tool for your own controlled, reviewed schema changes is still your problem, which is the subject of the database migration tools guide.
- An API you can hand to third parties. The generated client API is designed for your own application, not as a public contract with versioning, rate limits and a developer portal, which is the API management layer and a separate purchase.
The five escape hatches
Evaluate every platform in this list against these five, in this order. The order matters, because the first two are the ones with no workaround.
1. Data escape: what is underneath
The question is whether the datastore is a standard engine you could run yourself, or a proprietary store that exists only inside the vendor.
If the answer is Postgres, your exit is a dump and a restore. It is a long afternoon, the destination options are numerous, and everything you know about indexing, query plans and backups transfers. If the answer is SQLite, it is a file. If the answer is a proprietary document store, the export is a set of files in the vendor’s own format, and turning them into something another system can serve is a data engineering project plus a rewrite of every query in your application, because the query semantics do not transfer either.
That second case is not merely slower. The application code is coupled to the store’s capabilities: if the store cannot join, your data model has denormalisation baked in to compensate, and moving to a relational engine means undoing modelling decisions that are spread across your entire codebase.
2. Query escape: can you bypass the generated API
Generated APIs cover the common cases well and then stop. The question is what you do when you need something outside them: an aggregate across several tables, a recursive query, a window function, a bulk operation, or a query whose plan you need to control.
The best answer is a direct connection to the database, so you can run whatever SQL you like from a server you control, alongside the generated API rather than instead of it. That turns the abstraction into a convenience you opt out of per query, which is the correct relationship to have with an abstraction.
The worst answer is that all access is through the SDK and the platform’s query language, in which case an unsupported query has no workaround other than restructuring your data or moving the computation into the client, which is usually both slow and a data exposure.
3. Compute escape: running code the platform did not anticipate
Function runtimes on these platforms are typically constrained: a specific language, a maximum execution duration, limited or no filesystem, a restricted dependency set, and cold starts. That covers webhooks, validation and light integration work.
What you need to know is what happens beyond that. Can you run an ordinary long-lived server alongside, connected to the same database with the same authorization context? If the platform’s data layer is reachable from arbitrary compute, you can put anything next to it, and the BaaS becomes one component in your architecture rather than the whole of it. If the data layer is only reachable through the platform’s own runtime, your architecture is bounded by that runtime’s limits permanently.
4. Identity escape: whether your users can come with you
This is the hatch that surprises people, and it is the most expensive one to get wrong.
Migrating users means moving their credentials. If the platform lets you export password hashes along with the algorithm and parameters used to produce them, and your destination can verify that algorithm, migration is invisible to users: they sign in as normal and it works. If it does not, every user must reset their password, which means an email campaign, a support spike, and permanent loss of the segment of your user base that does not bother. For a consumer product that segment is not small.
There is a middle path worth knowing about, because it is the standard answer when hashes cannot be exported: a migration hook on the destination that, on a failed sign-in for an unknown user, authenticates against the old system, and on success creates the user locally with the freshly supplied password. It works, it requires keeping the old system running and paid for during the migration window, and it only migrates users who actually sign in during that window.
So the question to ask of every platform is precise: can I export the password hashes and the parameters. Not “can I export users”, which every platform says yes to while meaning email addresses. The same reasoning applies when choosing any authentication provider, and it applies more sharply here because the identity system arrived as a side effect of choosing a database.
5. Deployment escape: is self-hosting real
“Open source” and “self-hostable” are used loosely in this category. The useful questions are whether the open-source build is the same software the cloud runs or a subset, whether the features you depend on are in it or reserved for the hosted tier, whether anyone runs it at production scale, and what the upgrade path looks like when the cloud version moves ahead.
A genuine self-hosting path is valuable even if you never use it, because it changes the negotiation and it caps the worst case. A self-hosting path that exists as a repository nobody operates seriously is a marketing claim.
Needs first-hand data: For whichever platform you are evaluating, actually run the export before you commit, not after. Export the full dataset and the user records with credentials, then load them into a plain instance of the underlying engine and run your three most complex application queries against it. Record the wall-clock time for the whole exercise and whether the password hashes were usable. That half-day is the cheapest insurance in this entire category and almost nobody does it.
The abstraction you outgrow first is authorization
Teams expect to outgrow a BaaS on scale. They almost never do, because these platforms sit on engines that handle far more than most products generate. What they outgrow is the authorization model, and it happens early.
The client-side access model is the core idea of a BaaS: the client talks to the datastore directly, and a declarative rule layer decides what each user may read and write. In Postgres-based platforms that is row-level security. In document stores it is a rules language. Either way the promise is that authorization lives in one declarative place rather than in every endpoint.
It works beautifully for ownership. “A user may read rows where the user identifier column matches their session” is one line, enforced by the engine, impossible to bypass from the client.
It degrades in four predictable ways.
Relationship depth. “A user may read a document if they are a member of the team that owns the project the document belongs to” requires traversing several relations inside the policy. Expressible, and now the policy runs as a subquery on every row considered, which is a performance characteristic hidden inside a security feature. Policies with joins are the most common cause of a query that was fast in development and is not in production.
External state. Rules that depend on something outside the datastore, a feature flag, an entitlement in a billing system, a response from a permissions service, cannot be expressed at all. You now need a server in the path, which means the client-direct model is over for that data.
Field-level and conditional writes. “This role may update these three columns but not the other seven, and only while the record is in this state” is where declarative rule languages become long, unreadable and untested. The failure mode is not that it cannot be written; it is that nobody can review it.
Testability. Policies are usually tested by hand through a console. When authorization is the only thing between the internet and your database, hand-tested is not an adequate standard, which is the same argument the API security testing guide makes about object-level authorization, and here the stakes are higher because there is no application tier to catch mistakes.
The exit from this is consistent and worth planning for deliberately: keep client-direct access with declarative rules for the simple, ownership-shaped majority of your data, and route the complicated parts through a server you control that holds a privileged connection and implements authorization in code you can test. Platforms that allow a privileged server-side connection make that a gradual, per-table decision. Platforms that do not make it a rewrite.
Needs first-hand data: Take your most relationship-heavy access rule, implement it as a declarative policy, and measure query latency on a table with realistic row counts, with and without the policy enabled, at several relationship depths. Publish the curve. The point where policy evaluation starts dominating query time is the practical ceiling of the client-direct model for your data, and it is knowable in an afternoon rather than discovered in production.
Supabase

Supabase is Postgres with an ecosystem assembled around it: an automatically generated REST API over your schema, an authentication service, storage, realtime subscriptions driven by the write-ahead log, and a Deno-based function runtime. The defining property is that the database is an ordinary Postgres instance you can connect to directly, which makes almost every escape hatch above answerable in the affirmative.
Pros
- The datastore is real Postgres with a connection string, so any query the generated API cannot express is just SQL from a server you control, and extensions, indexes and query plans all behave normally
- Exit is a dump and restore into any Postgres host, which makes this the strongest data escape hatch in the category and removes the largest category of lock-in
- User credentials live in an ordinary table with standard password hashes, so migrating identity elsewhere does not force a password reset on your entire user base
- Genuinely self-hostable, with the same components the cloud runs, which makes residency requirements answerable without a contract negotiation
Cons
- Authorization is row-level security, so you inherit its performance characteristics, and policies traversing relationships get expensive in ways that are invisible until production data volumes
- The breadth of the platform means depth varies, and several components are less mature than the Postgres core they surround
- Connection management is a real concern because serverless clients plus direct Postgres connections is a known failure mode, so pooling has to be understood rather than assumed
- Self-hosting the full stack is a multi-service deployment, which is meaningfully more work than the single-binary options here
Best for: Teams who want the speed of a BaaS without accepting proprietary storage, and who are comfortable that the ceiling and the exit are both ordinary Postgres problems.
Pricing: Open source with no licence cost when self-hosted, plus a managed tier with a free entry level and usage-based metering on compute, storage, bandwidth and monthly active users.
Appwrite
Appwrite is an open-source backend platform designed to be self-hosted from the start, bundling authentication, a database abstraction, storage, functions in many runtimes and realtime behind a consistent API and a set of client SDKs. Where Supabase exposes the database, Appwrite deliberately abstracts it, presenting collections and documents through its own API regardless of what is underneath.
Pros
- Self-hosting is the design centre rather than an afterthought, so running the same software the cloud runs is a supported and well-trodden path
- Function runtimes cover an unusually wide range of languages, which removes the “everything must be TypeScript” constraint several competitors impose
- One coherent API and SDK surface across auth, data, storage and functions, which keeps client code simple and consistent
- Permissions are attached to documents and collections in a model that is simple to reason about, which makes the common ownership cases straightforward and reviewable
Cons
- The database is an abstraction rather than an exposed engine, so complex relational queries, aggregates and joins are constrained by what the API supports rather than by SQL
- No direct connection to the underlying storage means the query escape hatch is weak, and an unsupported query has to be restructured rather than written
- Data export is in the platform’s own shape, so moving to a different system is a transformation project rather than a restore
- Smaller ecosystem than Firebase or Supabase, which shows in third-party integrations and in how much operational knowledge exists publicly
Best for: Teams whose hard requirement is running the whole backend on their own infrastructure, with a document-shaped data model that fits the API.
Pricing: Open source with no licence cost when self-hosted, plus a managed cloud tier with a free level and usage-based metering on requests, storage and bandwidth.
Firebase
Firebase is the platform that defined the category and it remains the most complete: Firestore and the realtime database, authentication with a long list of providers, cloud functions, storage, messaging, crash reporting and app integrity checks, all integrated with the wider cloud platform behind it. For a mobile-first product that needs offline support and realtime sync, nothing here matches its maturity.
Pros
- Offline persistence with automatic conflict handling and sync on reconnect is genuinely hard to build and is the strongest single reason to choose it for mobile
- The realtime and subscription model is the most mature in this list, with client SDK behaviour that is well understood across platforms and network conditions
- The surrounding ecosystem, covering messaging, analytics, crash reporting and app integrity, means fewer separate vendors for a mobile product
- User credentials can be exported with their hashes and algorithm parameters, so identity migration to a system supporting that algorithm does not force a global password reset
Cons
- Firestore is proprietary with no equivalent to run elsewhere, so the data escape hatch is the weakest here and the exit is a rewrite rather than a restore
- The query model has hard limits, notably no joins and constrained filtering, which forces denormalisation decisions into your data model that are painful to unwind later
- Pricing meters document reads, which couples your bill to how your data is modelled rather than to how much data you have, and a modelling mistake becomes a recurring cost
- The security rules language is its own thing to learn and test, and rules that depend on reading other documents incur their own billed reads
Best for: Mobile-first products where offline sync and realtime are core requirements and the proprietary datastore is an accepted, deliberate trade.
Pricing: Usage-based metering on document reads, writes and deletes plus storage and bandwidth, with a free entry tier and no per-seat component.
Nhost
Nhost assembles a Postgres-based stack around GraphQL rather than REST: Postgres for storage, Hasura generating the GraphQL API with its declarative permission model, an authentication service, storage and functions. It is a coherent choice for teams who want GraphQL as the client contract without operating the pieces, and the components underneath are individually replaceable.
Pros
- Postgres underneath means the same strong data escape hatch as any Postgres-based platform, with a dump and restore as the exit
- A generated GraphQL API with a declarative permission model gives clients precise data fetching without a hand-written resolver tier
- Every major component is independently available, so leaving the platform is a matter of running the same pieces yourself rather than replacing them
- Self-hosting is supported, which keeps residency and control questions answerable
Cons
- You inherit Hasura’s model and its evolution, including major-version changes to the engine and metadata model, which is a dependency you do not control
- GraphQL brings its own operational surface, covering query cost limiting, persisted operations and per-operation observability, as the GraphQL platforms guide details
- Smaller company and community than the leaders here, which is a continuity consideration for something this central
- Permission logic split between Postgres row-level security and Hasura metadata means two places to reason about authorization rather than one
Best for: Teams who want GraphQL as their client contract on top of Postgres and prefer an assembled stack of replaceable open components.
Pricing: Open source components with no licence cost when self-hosted, plus a managed tier with a free level and usage-based metering on compute, storage and bandwidth.
PocketBase

PocketBase is a single Go binary containing SQLite, an admin interface, authentication, file storage, realtime subscriptions and a REST API. You download a file and run it. It is also usable as a Go framework, so you can embed it in your own application and add arbitrary handlers, which is an unusually clean compute escape hatch.
Pros
- One binary and one file makes deployment, backup and migration trivial, and the exit is copying a SQLite file that any tool on earth can read
- Embedding it as a Go framework means arbitrary server-side code runs in the same process with direct database access, which removes the runtime constraint other platforms impose
- Operational simplicity is genuinely unmatched here: no cluster, no connection pooling, no multi-service deployment, no control plane
- Direct SQLite access means any query you can write in SQL is available, with none of the generated-API ceiling
Cons
- Single-node by construction, so horizontal scaling and high availability are not available, and the ceiling is one machine’s capacity
- SQLite’s write concurrency model means write-heavy workloads serialise, which is a hard architectural limit rather than a tuning problem
- A small maintainer base for something at the centre of your product, which is a real continuity consideration
- No managed offering, so operations, backups, upgrades and availability are entirely yours
Best for: Small products, internal tools and side projects where operational simplicity is worth more than horizontal scaling, and single-node is an honest fit.
Pricing: Open source with no vendor and no bill for the software. Your only cost is the single server you run it on.
Convex
Convex takes a different position: instead of a database with rules and a separate function runtime, queries and mutations are TypeScript functions that run inside the platform with transactional guarantees, and the client subscribes to query functions so results update reactively when underlying data changes. Authorization is code inside those functions rather than a declarative rule language, which is a direct answer to the problem described earlier.
Pros
- Authorization is ordinary TypeScript inside query and mutation functions, so complex rules are testable code rather than an inexpressive rule language
- The reactive query model removes an entire category of cache-invalidation and subscription-management code from the client, which is a real and underrated simplification
- Transactional mutations with deterministic execution make consistency guarantees strong in a way document-store platforms generally do not offer
- Functions are the only data path, which means there is no client-direct access to secure, eliminating the class of misconfigured-rule exposures
Cons
- A proprietary data model and query interface, so the data escape hatch requires rewriting every query and reshaping the data rather than restoring a dump
- No SQL, so analytical queries, reporting and any third-party tool expecting a database connection need a separate export pipeline
- The programming model is opinionated and different enough that experience elsewhere transfers imperfectly, which is a real onboarding cost
- Younger ecosystem with fewer integrations and less public operational experience than the established platforms
Best for: Product teams building reactive applications who want authorization and business logic as tested code, and who accept a proprietary datastore in exchange.
Pricing: Usage-based metering on function calls, database bandwidth and storage with a free entry tier and tiered plans above it.
AWS Amplify
Amplify is a toolchain and hosting layer that provisions ordinary AWS services underneath: identity in Cognito, data through a managed GraphQL layer over DynamoDB, storage in S3, functions in Lambda. That is its central property, and it cuts both ways. The resources are real AWS resources in your own account, which is the best possible answer to “what is underneath”, and the generated infrastructure is complex enough that taking manual ownership of it is a serious undertaking.
Pros
- Everything provisioned lives in your own AWS account as standard services, so there is no vendor holding your data and no export required to keep it
- Scaling, availability and regional behaviour are the underlying services’ rather than a startup’s, which removes a category of risk for a long-lived product
- Integrates directly with the rest of AWS, so a queue, a workflow engine or a container service is an addition rather than an escape
- Your existing account controls, identity federation, audit logging and compliance posture apply automatically, which is frequently the strongest argument in a regulated environment
Cons
- Cognito user pools do not let you export password hashes, so migrating identity away requires either a sign-in-triggered migration hook running against the old pool or a forced reset for every user
- The generated infrastructure is intricate, and moving from Amplify’s tooling to managing those resources directly is a significant project rather than a switch
- DynamoDB’s access patterns must be designed up front, and a data model that fits the generated API rarely fits the queries you need two years later
- The developer experience is more complex than the single-purpose platforms here, and the number of moving parts shows quickly
Best for: Teams committed to AWS who want a generated backend on services they already run, and who value account ownership over developer-experience polish.
Pricing: No platform fee beyond usage of the underlying AWS services, metered individually, plus hosting and build minutes for the front-end layer.
How to choose
Start with the identity question, because it is the least reversible. Ask each candidate whether you can export password hashes with their algorithm parameters. Supabase and Firebase answer yes in different ways. Cognito answers no and offers a migration hook instead. That single answer determines whether leaving costs you a migration or a percentage of your user base.
Then decide whether the datastore must be standard. If your product will plausibly need analytical queries, third-party tooling, or a move to a managed database provider later, choose something on Postgres or SQLite and accept a less magical developer experience. If your product is genuinely mobile-first with offline sync as a core requirement, Firebase’s advantage is real and the proprietary store is the price.
Then check the compute escape. Can you run an ordinary server with privileged access to the same data. If yes, the platform is a component and your architecture stays open. If no, your architecture is permanently bounded by the function runtime’s limits, and every future requirement gets evaluated against them.
Then be honest about scale. Most products never outgrow single-node SQLite, and PocketBase exists for exactly that population. If you are one of them, the operational simplicity is worth more than the headroom you will not use. If you genuinely are not, do not pick it as a bet on staying small.
Do the export rehearsal before you build anything substantial. It takes half a day and it converts the most important property of this decision from a claim into a measurement.
| Platform | Datastore | Direct DB access | Password hash export | Self-host | Picks itself when |
|---|---|---|---|---|---|
| Supabase | Postgres | Yes | Yes | Yes, full stack | You want BaaS speed with an ordinary Postgres exit |
| Appwrite | Abstracted | No | Platform-dependent | Yes, by design | Running everything on your own infrastructure is mandatory |
| Firebase | Proprietary | No | Yes, with parameters | No | Offline sync and mobile realtime are core requirements |
| Nhost | Postgres | Yes | Yes | Yes | GraphQL is the client contract you want |
| PocketBase | SQLite | Yes | Yes | Yes, it is the only mode | Single-node is an honest fit and simplicity is the priority |
| Convex | Proprietary | No | Platform-dependent | Limited | Authorization belongs in tested code, not a rule language |
| AWS Amplify | DynamoDB in your account | Yes, via AWS | No, migration hook instead | n/a, it is your account | You are on AWS and account ownership outranks polish |
Frequently asked questions
Will I outgrow a backend-as-a-service platform?
Probably not on scale, and very likely on authorization. These platforms sit on engines that handle far more load than most products generate. What teams actually hit is a rule the declarative permission layer cannot express, or a query the generated API cannot produce. Both are survivable if you can route those cases through a server with privileged data access, which is why the compute and query escape hatches matter more than any throughput number.
How hard is migrating users off a BaaS?
It depends entirely on password hashes. If you can export them with their algorithm and parameters, and your destination can verify that algorithm, users notice nothing. If you cannot, every user must reset their password, which means a campaign, a support spike and permanent loss of the users who do not act. The middle path is a migration hook on the new system that authenticates against the old one at first sign-in, which works but requires keeping the old system running and only captures users who sign in during the window.
Is self-hosting a BaaS actually practical?
For some, yes. PocketBase is one binary. Supabase, Appwrite and Nhost are multi-service deployments that are genuinely run in production by real teams, with real operational work involved in upgrades, backups and availability. The question worth asking is whether the open-source build is the same software the cloud runs or a subset, because a self-hosted deployment missing the features you depend on is not an escape hatch.
Should I use row-level security for authorization?
For ownership-shaped rules, yes, and it is the strongest available guarantee because the engine enforces it and the client cannot bypass it. For rules that traverse several relationships or depend on state outside the database, it degrades on both performance and readability. The sustainable pattern is row-level security for the simple majority and a server-side path with privileged access for the complicated minority, decided per table rather than as a global architecture.
Can I use a BaaS as the backend for a public API?
Not directly. The generated client API is designed for your own application: it has no versioning story, no per-consumer rate limits, no developer portal, no deprecation tooling and a contract shaped by your schema rather than by a product decision. Put a real API layer in front of it, which is the API gateway and management question, and keep the generated API for your own clients.
Related reading
- Best API management platforms — the layer you need if anyone outside your own application will call this.
- Best managed Postgres providers — where your data lands if you take the Postgres exit.
- Supabase vs Firebase vs Neon — the three-way comparison in more depth for the most common shortlist.
- Best authentication providers — the identity decision you made by accident when you picked a database.
- Best GraphQL servers and platforms — what you take on if the generated API is a graph.
- Best database migration tools — schema change management, which no BaaS solves for you.