All posts

Moving ThisThat off Firebase while campaigns were live

A live SaaS migration from Python Firebase Functions to Node.js and PostgreSQL — parity tests, module-by-module cutover, and why we did not big-bang it.

  • Node.js
  • migration
  • PostgreSQL
  • multi-tenant
  • SaaS
Serverless to Node.js live migration illustration

ThisThat runs brand campaigns where users vote between paired images in tournament rounds. When I joined remotely, the backend was Python on Firebase Functions. It handled traffic. It also got harder to extend every time marketing wanted a new campaign shape, and cold starts made latency unpredictable.

The plan was Node.js, PostgreSQL, Sequelize, and a codebase the small team could actually share. The catch: campaigns were live. Brands were mid-competition. "We will fix it in prod" was not on the table.

Matching Firebase output, function by function

We did not start by rewriting everything at once. We listed every Firebase function, mapped each one to a NestJS route and Sequelize model, and recorded real production payloads. Parity tests compared old and new outputs for pair selection, vote recording, round progression, and winner calculation.

The bar was exact behavior, not "close enough." A rounding difference in vote counts is not a refactor detail when a brand is running a paid campaign. Multi-tenant isolation had to get stricter too — each brand's tournaments, assets, and results partitioned on shared infrastructure with no cross-brand leaks when we moved traffic.

Cutover happened one module at a time. Compare outputs, migrate rows in batches, watch error rates, fix forward when something drifted. No weekend flag flip.

Campaign setup was half the migration

A lot of the pain was not the voting API. Before migration, launching a campaign often meant developers in the loop: upload image pairs, configure rounds, set brand-specific rules, copy config between environments. Admin tooling was scattered across Firebase consoles and one-off scripts.

We moved campaign configuration into one panel — image pairs, round structure, brand settings, publish windows. Marketing could stand up a tournament without filing a developer ticket for every launch. That cut manual setup work by roughly thirty percent. For a product that shipped new campaigns weekly, that mattered as much as removing cold starts.

What improved after cutover

We reached functional parity without breaking live campaigns. Average API latency dropped about twenty percent once cold-start variance disappeared and query paths tightened. One Node codebase let the team ship admin and voting changes in the same pull request instead of coordinating Python functions and separate tooling.

Serverless still fits spiky, isolated handlers. Tournament state with relational invariants, admin CRUD, and live campaign edits wanted a long-running service and PostgreSQL. Same playbook if I did it again: parity tests first, module cutover second, no heroics on a Friday night.