← All case studies Firebase → PostgreSQL

Traba spent a year moving Firestore collections to Postgres

Traba's engineering post walks a year of collection moves off Firestore onto Aiven Postgres with Prisma. Triggers plus reconciliation hit 99.99% precision in April. The shifts cutover flipped reads and writes together because replication lagged about a second. Firestore stayed for flags and cron.

Traba’s engineering team published this account of leaving Firestore for Postgres. The page does not print a publication date. The timeline inside it runs January through the following December, one year, and ends when the workers collection moves. Treat the months as their sequence, not as a calendar year I can pin down.

This is not a full Firebase exit. At the end they still use Firestore for a light feature-flag store and for other NoSQL that still fits. Cron jobs that precompute metrics, and scheduled tasks, stay on Firebase. The Firebase → PostgreSQL playbook covers auth, storage, and functions as well as documents. Their post is the document move.

A schema they threw away in 2021

In summer 2021 the backend was a spreadsheet. CTO Akshay and founding engineer Moreno had a relational schema. A meeting with the mayor of Miami, about an app that would promise jobs to Floridians, was three weeks out. They dropped the schema and stood up Firebase in a day.

Firestore was the right speed then. Flexible documents, horizontal scale, functions, cron, a local emulator, a free tier, and fast single-document reads. Traba is a staffing platform. Businesses in warehousing, manufacturing, and distribution post shifts. Workers claim them in the mobile app. The collections were companies, workers, shifts, and shift_signups, plus an associations collection for whether a company had favorited or blocked a worker.

A year and a half in, the queries hurt. Operations needed a worker whose name contained a substring like “ave”. Firestore could not do that. The server loaded every worker and filtered in memory. At hundreds of thousands of documents that hit memory limits. A more normal operations query was worse. Workers who worked a shift last week, who are not on an active shift, and who are not blocked by one company. That was several round trips and a lot of client-side stitching. Other Firestore limits they list: no count without reading every document or maintaining a counter, no LIKE, an in operator capped at 30 values, and no query with more than one inequality, because that would scan more than one index.

They counted about 20 endpoints stuck this way. Algolia or Elasticsearch could have faked the fuzzy search. Denormalized copies could have made the popular joins cheaper. They did not want either as the long-term model.

Mongo, then MySQL, then Postgres on Aiven

MongoDB lost quickly. It would dodge some Firestore rules and still be the wrong shape for relational data. MySQL versus Postgres took longer. They mention Slack and HubSpot as MySQL shops that stayed. Postgres won on current writing, and on stories they had read about MySQL defaults that leak into application code. They wanted the migration to be as unsurprising as they could make it.

Hosting took a week of research. Cloud SQL would have stayed inside GCP, where they already were, and exposed more knobs. Aiven answered fast and put a solutions architect on the call. They had been considering Kafka to land Postgres in BigQuery. The architect showed a path through Google Cloud Datastream and Aiven that avoided a Kafka bill they estimated at about $500 a month. Aiven also meant managed backups, replication, a pooler, and a say in when Postgres upgrades landed. They picked time saved over fine-grained control.

The ORM choice was faster. They wanted migrations and types. TypeORM versus Prisma. Prisma had the docs, raw SQL when they needed it, a busy Slack, and a monthly release. They had also read about teams that found it less stable than TypeORM, and about Go support that Prisma added and then dropped. The query engine was not the fastest they looked at. They still picked Prisma, and they did not think TypeScript support was about to disappear.

Triggers, then a race, then a cron that heals

Two ways to copy new writes.

Shadow writes would do a second write to Postgres at every Firestore call site in the Node server. The code would already be in the request path, so turning a collection off later would be a delete. Connection use would be visible. The cost was finding every call site up front, training everyone to remember the second write, harder retries, and extra latency on the request.

Triggers looked faster to start. A Firestore trigger runs a function that translates the document into a row. The user request does not wait. Retries sit in the function, not in the API. The later pain is real. You write the mapping once in the trigger, then again in the Node server when that collection’s writes move for good. Each collection’s trigger also takes its own connection, so four related writes are four pool checkouts instead of one transaction. And the lag means you cannot read Postgres for anything that needs the write you just did.

They liked triggers, if the pool could take it. Then they found the race.

A shift has three openings. Worker one joins. Two replications fire, the new shift_signups row and an increment of workerOpeningsFilled on the shift. Both land. Workers two and three join at nearly the same time. The signup rows land. The shift updates do not stay in order. Cloud functions do not promise order. A cold start on the first update lets the second finish first, and Postgres keeps workerOpeningsFilled at 2.

They split that into two failure modes. Wrong field values got a reconciliation script per collection, comparing Firestore to Postgres and upserting. They ran those by hand at first. Critical collections moved to a cron. Early on, a script took a few minutes. Near the end, some took 20 minutes or more from a VM close to both databases, and an hour or more from a laptop. Constraint failures were the other mode. A new worker creates a worker document, a profile, and a metrics document. In Postgres the worker row has to exist first. If the profile trigger wins the race, the insert fails. Their retry loop tried again, often 15 times or more, and a Slack bot fired if it still failed. Across the whole year the bot pinged them 0 times.

Backfill reused the same translation as the triggers. They did not move every collection at once. Small ones first, to learn the flow. Medium ones in the middle. shifts, shift_signups, and workers last. Product work continued. Nobody was full time on the migration. About 12 engineers by the end.

The year, and the morning they flipped shifts

January was a load test of replication. Concurrent functions could drop pool connections, so they resized Aiven before the real traffic. Then they wrote the triggers.

February and March finished the first full collection, worker_metrics. In Firestore those metrics were assembled in code. In Postgres they were one query.

April’s check was 99.99% precision on the replica. They also shipped worker search entirely on Postgres. Search could tolerate a couple hundred milliseconds of lag, which the operations team had been waiting on, because the Firestore version kept getting slower as signups grew.

May through October moved the bulk, including users and companies, while new features went to Postgres first.

October through December was shifts and shift_signups. Shifts touch the whole business. Signups touch worker payments. They did shifts first because the table was a bit smaller, the lessons would transfer, and they wanted to try flipping reads and writes in the same moment.

Mike Staunton went through hundreds of shifts call sites. Transactions, side effects, and connection counts were the gotchas. For earlier collections they had turned on Postgres reads, let them bake, then moved writes. Shifts could not work that way. Trigger lag was about a second. If reads were already on Postgres and writes were still on Firestore, a shift with 9 of 10 openings filled could still read as 9 while a second worker joined. You would end with 11 people on a 10-person shift.

They wrote a rollback that could copy Postgres back to Firestore and freeze requests while it did. Artillery hammered a dev environment that was already on Postgres. Javier Rodriguez, who ran analytics, did one more validation pass. On a cold Thursday morning in New York they asked operations for the quietest window. They played “Trinity” from the Oppenheimer score and flipped both flags on the explosion. The rollback script went unused. Worker-opening fields still drifted, and performance problems showed up that Artillery had not caught. Late nights followed. shift_signups used those lessons in the following weeks. workers was the last collection, a year after they started. They call the downtime zero.

Connections, a month of cleanup, and a bill

Firestore had never made them think about connections. A .map() that fetched documents became N Prisma calls and N pool checkouts. Spikes showed up, and they switched those paths to batches.

About eight months in, the backend itself was the blocker. Dependency cycles, Firestore calls with no data-access layer, duplicated code. They spent a month removing cycles, collapsing copies, and moving every Firestore read into a collection file. Migrating a collection then meant translating the functions in one file. They also deleted old Firestore triggers in that pass.

The wins they publish: engineers did not ask to go back to the Firestore API, dashboards got faster, and critical read-heavy endpoints went from more than 10 seconds to under 1 second. Firestore bills on reads, writes, and deletes, so growth had been expensive. They say the move cut that bill by 70 percent or more, and the Postgres bill is hourly rather than per operation.

The lesson they underline is the reconciliation window. Scripts that took minutes at the start took 20 minutes later, and every new field had to be added to a trigger that was not the database the code was reading. Waiting longer could have made the cron unfit to run. Enforcing “add the field to the trigger” across 12 people was its own project. The names they thank on infra are Allison Hojsak, Arvind Anand, Mike Staunton, Moreno Antunes, Nazer Hasanian, and Tara Nagar, plus Javier Rodriguez on the pipelines.