"Should we migrate off Firebase?" is usually the wrong question. The estates we review almost never need to leave Firebase; they need to move one or two specific workloads that have outgrown Firebase primitives, while Auth, Hosting, and most of Firestore stay exactly where they are. Cloud Run is the natural landing zone — it lives in the same GCP project, speaks to Firestore and Firebase Auth natively, and gives you a real server without giving up the platform.
The judgment call is knowing which workloads qualify. Here are the signals, and the wiring that makes the hybrid clean.
Signals a workload has outgrown Functions/Firestore
The workload fights the request model. WebSockets, server-sent events, gRPC streaming, long-lived connections of any kind — Cloud Functions is request/response, and workarounds (polling loops in Firestore, long-poll functions) show up in the read bill. Cloud Run supports streaming and WebSockets directly.
You are assembling a framework out of Functions. When an HTTP API grows past a dozen endpoints, teams start rebuilding routing, middleware, and shared context inside a single "api" function — at which point it is a server, deployed awkwardly. Moving it to Cloud Run with a normal framework (Express, Fastify, whatever your team knows) removes the awkwardness without changing the architecture; a container gives you your own dependency and runtime choices too.
The query patterns are relational. If a feature keeps needing multi-entity joins, ad-hoc filters, or transactional invariants across many documents, and you find yourself maintaining a lattice of denormalized projections to serve it — that feature wants Cloud SQL behind a Cloud Run service. Keeping it in Firestore means paying engineers to reimplement a query planner, one projection at a time.
Cost curves that bend the wrong way. Compute-heavy request paths (PDF rendering, image pipelines, report generation) price poorly as per-invocation Functions at scale, and CPU-intensive work benefits from Cloud Run's instance-based billing with concurrency — many requests amortizing one warm instance.
Counter-signals matter too. Event-driven glue (a thumbnail on upload, a welcome email on signup) is exactly what Functions are for. And workloads that are actually fine — merely unfamiliar — should stay. The bar is a concrete limitation with a bill or an incident attached, not aesthetic discomfort.
The wiring: keeping Firebase Auth authoritative
The hybrid's most important property is that identity stays unified. A Cloud Run service verifies the same Firebase ID tokens the client already holds:
import { initializeApp } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
initializeApp();
async function requireUser(req) {
const header = req.headers.authorization ?? '';
const match = header.match(/^Bearer (.+)$/);
if (!match) throw new HttpError(401, 'missing token');
return await getAuth().verifyIdToken(match[1]); // uid, claims — same identity
}
The client keeps signing in with Firebase Auth and attaches its ID token to requests to the new service, exactly as it does for callable Functions. Custom claims keep working as the authorization backbone on both sides of the seam. No second identity system, no session migration.
Route the service through Firebase Hosting rewrites ("run": {"serviceId": ...}) or a load balancer so clients see one origin. This also means the workload can move back, or move again, without a client release.
The data seam
Decide explicitly which store owns each entity after the split. The stable patterns:
- Cloud Run reads/writes Firestore directly via the Admin SDK — no security rules apply server-side, so the service enforces authorization in code. Simplest seam; right when the workload is compute-bound, not model-bound.
- The workload's entities move to Cloud SQL, and Firestore keeps a thin projection if clients still need real-time updates on that data. The service dual-writes during transition, with a reconciliation job comparing stores until the numbers hold.
- Firestore stays the write path; BigQuery or SQL becomes the read path for analytical or reporting features, fed by change streams. No dual-write risk — the pipeline is one-directional.
Whichever seam you pick, write the reconciliation check before the cutover, run old and new paths in parallel against production traffic, and only then move reads. The check is not ceremony; on most migrations it catches at least one silent divergence (timezone handling and null-vs-missing fields are the usual suspects).
What this buys you
A team that moves its one problematic workload to Cloud Run typically keeps 80–90% of its stack on Firebase and stops having the argument about leaving. The prototype-friendly parts of the platform stay doing what they are good at; the workload that needed a real server gets one; and the seam — Firebase Auth tokens plus a Hosting rewrite — is small enough to explain on a whiteboard. That is what "outgrowing Firebase" should usually mean: not an exodus, a boundary.