ENFR

FoodChéri & Seazon · NX, React, Node.js · Architecture

Ending the copy-paste between two brands

  • NX
  • React
  • Node.js
  • Express
  • CircleCI
  • Docker
  • GCP
  • ~75%more deploys
  • 6 → 1repos consolidated

The problem, in one line

FoodChéri and Seazon shared real code (payment logic, utilities, components) but lived in separate projects. Updating a shared function meant copy-pasting the same change into each one, by hand.

Context

We ran three products (Seazon, FoodChéri, and a shared back-office), each with its own React frontend and Express backend: six repos in total. The split was historical. Seazon and its back-office came first, in React/Node. FoodChéri was originally plain PHP. When we decided to unify the whole stack on React, Node, and MongoDB, we migrated FoodChéri to match, using Seazon as the reference, and merged FoodChéri’s back-office into Seazon’s so a single back-office served both brands.

The result: a lot of shared code across six repos, with no real way to share it other than copying.

When the risk became real

The problem stopped being theoretical the day a developer updated our Stripe integration on Seazon and, understandably, forgot that the same logic also lived on FoodChéri and in the back-office, where certain actions trigger payments and refunds. The change landed in one place and not the others. On FoodChéri, some payments failed, and the affected orders for that day were cancelled.

When shared payment code is really three copies, staying in sync depends on every developer remembering all three, every time. That’s not a mistake that might happen; it’s a certainty. And it had just happened, on the payment path.

The trade-off I weighed

A monorepo isn’t free. It makes tooling, CI/CD, dependency management, and build performance more complex than separate repos. Changes can also have a wider impact (one edit can touch several applications), so you need solid testing and clear ownership to avoid breaking something. I knew from the start that it meant more upfront investment in build tooling, automation, and repo organization.

I accepted that cost for one reason: we shared a lot of code. A single source of truth for shared components, utilities, and types would remove the duplication, keep everything in sync, and make cross-project changes far easier to coordinate. We were trading operational complexity for consistency and long-term productivity.

The alternative I rejected

Before choosing a monorepo, I considered publishing the shared code as private npm packages. It’s a valid approach. But it has its own cost: a publishing workflow, versioning, dependency management. Every shared change becomes a release, and every consumer has to bump a version to get it. Coordinating a change across several applications gets slower, not faster.

That cost only pays off when the applications belong to different teams, with different release cadences, and you’re building genuinely reusable libraries. That wasn’t our case. Our applications were built by the same team, evolved together, and shared a large amount of code. We weren’t publishing libraries for external consumers; we were building a single ecosystem. A monorepo let us change the shared code and all its consumers in a single commit, validate everything through one CI pipeline, and avoid the permanent overhead of publish-then-upgrade.

If those applications had belonged to independent teams with different release cadences, I’d have chosen private packages. Since ours were tightly coupled and changed together, the monorepo was the simpler operational model, even though it was the more complex one to set up.

How the migration went

Six separate repos with payment logic duplicated in three of them, consolidated into one monorepo with a single shared package

The structure was simple: the six applications live under apps/, and the shared code under packages/, split into shared-client and shared-server. The payment logic, utilities, components, and types that were copy-pasted now exist once, in packages/, and each app consumes them from there.

I sequenced the migration deliberately rather than moving everything at once: Seazon first (front and back), then the back-office, then FoodChéri. The order was a risk decision. Seazon was the reference implementation: FoodChéri ran on the same stack and largely the same code, so getting Seazon working in the monorepo effectively proved the pattern for FoodChéri before I even touched it. Each step made the next one safer.

Alongside the migration, I introduced automated CI/CD through CircleCI (Docker → CircleCI → GCP), replacing the separate manual deployment processes each project had before. NX’s affected graph meant CI only rebuilt and redeployed what actually changed, so consolidating into one repo didn’t cost us build time.

Results

  • The duplication is gone. Shared payment logic, utilities, components, and types now live once, in packages/, consumed by every app. The exact failure that cancelled a day of FoodChéri orders (a Stripe change applied to one copy and not the others) can’t happen that way anymore, because there are no longer three copies to keep in sync.
  • Deployment frequency rose ~75%, once automated CI/CD replaced the separate manual deployments.
  • Onboarding got noticeably faster: a new developer sets up and understands one repo instead of six.
  • Cross-brand consistency by default. A change to a shared component now reaches FoodChéri and Seazon in the same commit, instead of being copied between them and slowly drifting apart.
  • CI stayed fast. NX’s affected graph only rebuilds and redeploys what changed, so consolidation didn’t cost build time.

What I’d do differently

I ran the migration as a single cutover. It worked, but it was harder than it needed to be. The production apps couldn’t stop: the team kept shipping features and fixes to the separate projects, because those apps served customers and took orders. So the monorepo was a moving target: every change to the old projects made my migration go stale.

So once the structure was validated, I planned the cutover for a weekend: the only window with no orders in production and no developers working. I copied the latest version of each project into the monorepo, pushed to preprod, and tested all weekend. The cutover went to production on Saturday night. Monday surfaced a few bugs, but nothing serious and all fixable in minutes.

It worked, but I’d sequence it differently today. Rather than migrating all three projects and cutting over at once, I’d take each one to production before starting the next: Seazon, then the back-office, then FoodChéri. Same order, but one production cutover at a time instead of a single big bet. That would shrink the blast radius of each step to a single app and remove the all-or-nothing weekend where everything moves at once. The migration worked; the risk profile is what I’d improve.