Running Two CMSes in Production

How we built an API abstraction layer to migrate between CMS platforms without turning anything off

The Problem

Migrate from Umbraco to Payload CMS without downtime. Multiple brands, shared frontend codebase, independent migration timelines.

The Solution: API Response Alignment

The key insight was simple: if both CMSes return similar response structures, the frontend doesn’t need to care which one it’s talking to.

Here’s what a content row looks like from the legacy CMS:

Legacy CMS:

{
  "id": "hero-row",
  "widgets": [{
    "trackingId": "hero-row-01",
    "link": "/en/just-in",
    "desktop": {
      "aspectRatio": "2048:999",
      "imageUrl": "https://cdn.example.com/images/hero-desktop.jpg"
    },
    "mobile": {
      "aspectRatio": "1280:1666",
      "imageUrl": "https://cdn.example.com/images/hero-mobile.jpg"
    },
    "_alias": "imageWidget"
  }],
  "_alias": "contentRow"
}

The equivalent from the new CMS:

{
  "trackingId": "hero-row-1",
  "blocks": [{
    "blockType": "imageWithLinkBlock",
    "trackingId": "hero-row-01",
    "link": { "relationTo": "pages", "value": "abc123" },
    "desktop": { "relationTo": "media", "value": "img456" },
    "mobile": { "relationTo": "media", "value": "img789" }
  }]
}

Similar structure, different property names. That’s doable.

The Fallback Pattern

Built content models with intelligent fallback chains:

class ContentRow {
  private _response: LegacyCmsRow | NewCmsRow;

  get trackingId(): string {
    return this._response.id ?? this._response.trackingId ?? '';
  }

  get blocks(): ContentBlock[] {
    const raw = this._response.widgets ?? this._response.blocks ?? [];
    return raw.map(b => new ContentBlock(b));
  }
}

When a new store connects to the new CMS, the legacy properties are undefined, so the new ones take over. For the stores running on legacy system, it’s the reverse. Same code, different data sources.

Aligning the misalignment

Some differences couldn’t be resolved in the frontend. For those, a new proxy layer needs to be built on the backend to map the properties of the new system to be available for the frontend. This kept the “translation layer” server-side, where it belongs.

Benefits

  • Gradual migration: Stores moved to the new CMS one at a time
  • No downtime: Every site stayed live throughout
  • Rollback capability: If something breaks, a brand could switch back
  • Shared codebase: Frontend team ships features for all stores simultaneously

Trade-offs

  1. Some flexibility sacrificed in the new CMS to match legacy response shapes
  2. Extra proxy layer adding complexity
  3. Temporary hybrid state where migrated brands still depend on some legacy services

Nothing is free. Worth it? Absolutely. The alternative was months of frozen feature development during a “big bang” migration—with all the risk that entails.

Conclusion

CMS migrations don’t have to be all-or-nothing. With careful API design and a willingness to run both systems in parallel, you can migrate at business speed, not engineering speed.

The best migration is the one your users never notice.