DawnOps

Migrating to a Monorepo Without Coupling Deploys

We merged our API and platform UI into one repo last week. The goal wasn’t fashion; it was to remove two‑repo choreography without losing independent deploys.

The rule was simple: if the API and UI ever had to ship together, we wouldn’t do the move.

The constraint that mattered

  • API deploys must stay independent.
  • Platform deploys must stay independent.
  • CI must stay fast enough that people don’t avoid it.
  • Evidence has to fall out of normal work (PRs, CI runs, deploy logs).

If any of those were at risk, the migration was a “no.”

The layout we chose (and why it’s boring)

We used:

  • apps/api
  • apps/platform

It’s intentionally dull. Dull makes path filters easy and keeps the service boundary obvious.

Path-scoped CI is the hinge

Separate workflows, separate triggers:

  • API CI runs on apps/api/** plus shared paths.
  • Platform CI runs on apps/platform/** plus shared paths.

That cut the “build everything on every change” habit without weakening checks.

Separate images, separate deploy buttons

Each app publishes its own image tag:

  • ghcr.io/sam-dawnops/dawnops-api:sha-<sha>
  • ghcr.io/sam-dawnops/dawnops-platform:sha-<sha>

Deploys take a specific tag as input. That keeps releases explicit and audit-friendly. It also means the UI can stay pinned while the API moves, or vice versa.

How we migrated without losing history

  1. Created the new repo.
  2. Imported each repo with git subtree so history came along.
  3. Added the path-scoped workflows.
  4. Updated dawnops-meta to check out the monorepo paths.
  5. Marked the old repos read-only with a pointer to the new home.

Small gotchas we hit

  • Shared files don’t trigger path filters unless you add them.
  • Secrets have to move with the workflows or deploys quietly fail.
  • Platform production isn’t Cloudflare Pages. Keep that boundary obvious.

One rule that kept us honest

Every deploy takes a specific image tag or SHA. No “latest,” no hidden coupling.

If I had to do it again

Start from the deploy boundary, not the repo structure. Once the deploy boundary is protected, the rest is mechanics.

Keep going