Migrating a console nobody can stop using
How we moved an operations tool to a new app one screen at a time, with the old and new apps running side by side and talking over postMessage.
Our internal operations console was a React 17 app that the whole company used every day: merchant configuration, reports, settlements, compliance reviews. It needed replacing, but you can’t tell an operations team to stop working while you rewrite their tools.
So we didn’t. We built the new console alongside the old one and moved screens across one at a time.
Step one: embed, don’t replace
The first new screens didn’t launch as a new app at all. They appeared inside the old console, in an iframe, in exactly the place the old screen used to be. People kept their bookmarks, their sidebar and their muscle memory. Only the content of one page had changed.
An iframe on its own isn’t enough, though. The embedded page and the console around it have to agree on things.
Step two: a small message protocol
The two apps talk over window.postMessage. The embedded page sends a few typed events up to the parent:
ROUTE_CHANGE: the embedded page navigated, so the parent updates its address and breadcrumbs.SESSION_TIMEOUT: the session expired inside the frame, so the whole app logs out together, not just one rectangle of it.
The parent listens, checks that the message really came from its own frame, and reacts. Here’s a working version you can try. The inner page is a real iframe, and the log shows the real messages:
Session expired
The embedded page told the old console, so the whole app logs out together.
postMessage log
- Waiting for the embedded page…
Two details matter more than they look:
- Check the source. The listener ignores any message that doesn’t come from the frame it created. Anything can post a message to a window.
- Hide the chrome when embedded. The new app detects when it’s inside a frame and hides its own sidebar and header, while keeping every permission check. Otherwise you get two navigations stacked inside each other.
Step three: let people switch
Once enough screens existed, the new console became a real app with its own login and navigation. Both apps got a handoff: “Try the new view” in the old one, “Switch to old view” in the new one. Nobody was forced to move on a deadline. People moved when the new screen was better, which is the only kind of migration that sticks.
Step four: write the rules down
Porting about sixteen configuration screens by hand invites drift: one screen checks permissions differently, another formats dates its own way. I wrote a migration playbook covering the route map, permissions, where the old code lives and what “parity” means for the UI, and packaged it as a skill an AI coding agent could follow. Every port then started from the same checklist.
What worked
- Small steps you can undo. Each screen moved on its own, and each could move back.
- One protocol, two directions. A handful of message types covered almost everything.
- Users decide when they’re ready. The switch buttons did more for adoption than any announcement.