Cross Platform Migration
Both directions are legitimate. Consolidating is not automatically progress, and separating is not automatically an admission — each is right when the arithmetic says so.
- Typical duration
- Five to fourteen months
- Team shape
- Three to five senior engineers
- Starts with
- A three-week assessment

Screen by screen, with both versions shipping
The version of this that fails is the parallel rewrite: a new codebase built alongside the old apps, with a switchover date that moves twice and a roadmap frozen while everyone waits. Nobody plans it that way, and it happens anyway, because the apps keep changing.
We migrate incrementally instead. React Native and Flutter both embed into an existing native app, so screens move one at a time inside the shipping product — the app in the store is always the real one, and each moved screen is verified in the field before the next is scoped. Feature work continues throughout, which is the difference between a migration that finishes and one that gets cancelled at sixty per cent.
What usually goes wrong, and what we do instead
The technical work is well understood. The sequencing and the freeze are what go wrong.
Where platform migrations fail
- A parallel rewrite with a switchover date that moves and moves again
- A roadmap freeze announced for the migration, then quietly extended
- Behaviour differences between old and new found by users, not by tests
- The shared and native boundary decided screen by screen under pressure
- Store release mechanics rebuilt late, so nothing can ship mid-migration
How we run it instead
- Screens moved inside the shipping app, so there is only ever one product
- Feature work continues, with slices sequenced around the roadmap
- Behaviour characterised from the existing apps before anything moves
- The boundary drawn and written down before the first screen moves
- One release pipeline built in the first month, covering both stores
What changes, in practice
Taking consolidation as the common direction. The things that are different for the people who build and ship the apps.
Two apps today
Working, and everything happens twice.
- A feature is specified once and built twice, by two teams
- The two apps behave subtly differently and nobody knows where
- A bug is fixed on one platform weeks before the other
- Releases go out separately, and versions drift apart
- Hiring is two skill sets, and holidays are two single points of failure
- Business logic is implemented twice, so it diverges quietly
One codebase after
Same product, built once.
- A feature is specified once, built once and tested once
- Both platforms run the same business rules by construction
- A fix reaches both platforms in the same release
- One pipeline produces both store artefacts, so versions match
- One skill set to hire for and to cover
- The native boundary is documented, so what is not shared is deliberate
What the work involves
Whichever direction it runs, the shape of the work is the same.
Boundary design
What ends up shared and what stays native, drawn and written down before the first screen moves.
Characterisation
Tests written against what each existing app actually does, including where the two already disagree.
Incremental embedding
Screens moved inside the shipping app, so the store always holds the real product.
Native module ownership
The platform code that survives the migration, brought under test and documentation rather than inherited.
Field verification
Each moved screen measured on the slowest supported device and in the hands of real users before the next.
Reversibility
A moved screen can be routed back to the native implementation, and that path is tested rather than assumed.
How the migration runs
The same five stages as every engagement, applied to apps that cannot stop shipping.
Diagnose
What the apps do, where they already differ, and which screen is both valuable and safe to move first.
Decide
The direction, the framework, the boundary and the screen order, written down with what each one risks.
Prove
One screen moved and shipped to both stores inside the existing app, then routed back once to prove the path.
Deliver
Screens moved in sequence, each verified in the field before the next is scoped.
Hand over
Your team ships a release with us watching, then without us, and the replaced code is removed once nothing calls it.
What we move apps onto
The target depends on the direction and on who maintains it afterwards.
Shared
- React Native
- Flutter
- TypeScript
- Expo
Native
- Kotlin
- Swift
- Jetpack Compose
- SwiftUI
Data and services
- SQLite
- Firebase
- PostgreSQL
- GraphQL
Delivery
- GitHub Actions
- Gradle
- TestFlight
- Sentry
What you get, and when
Handed over as it is produced, not assembled at the end.
- Where the two apps already behave differently, found rather than assumed
- The boundary, and the screen order with its reasoning
- What would change the plan, stated before it happens
- Source in repositories you own
- Characterisation tests pinning the original behaviour
- A tested route back to the native implementation
- One pipeline producing both store artefacts
- Crash and performance dashboards split by platform and screen
- A record of what has been replaced and what has been removed
The rest of our cross-platform work
Shared-codebase engagements overlap more than most. These are the neighbouring ones.
Related work
Engagements where the apps could not stop shipping while they changed.
What we work towards
ILLUSTRATIVE, pending sign-off. These are the measures a platform migration is judged on and the direction they should move — not results from a named engagement. They will carry real figures or be removed before launch.
Implementations per feature
Gap between platform fixes
Store versions in step
Roadmap paused during the work
Rollback for a moved screen
Questions about platform migration
The ones we are asked most often before a first conversation.
How do we know consolidating is worth it?
Why not build the new app fresh alongside the old ones?
Do we have to pause feature work?
We want to go the other way — one codebase back to native. Is that odd?
Could we end up permanently half and half?
How long does this take?
Two codebases that should be one, or the reverse?
Tell us what you run today and which way you are leaning. We will tell you what the sequence should be.

