Organizing your team
Team shape, hiring vs upskilling, the platform team, and ownership of the native-Flutter boundary.
How you organize the team during add-to-app affects almost everything that follows: how fast you ship, how clean the boundary code stays, who owns the shared infrastructure, and whether the migration ever gets finished. In this chapter, we describe the setups we have seen work and a few choices we would argue against.
PoC phase
It's tempting to hand the first Flutter screen to the existing native team - they already know the app, the build pipeline, and the integrations. The trouble is that the hardest architectural decisions in add-to-app are made in those first few weeks: which engine model to use, which navigation pattern, what the contract format is, and how state flows across the boundary. Native devs without Flutter experience tend to pick whatever seems obvious at the moment, and such choices can get expensive once the project scales. We recommend bringing in at least one senior Flutter engineer for the PoC - in-house or from a partner - even if native devs still do the bulk of the work.
Those early decisions tend to stick. We've watched it happen: a PoC that was meant to be thrown away before the real migration becomes the actual starting point, and the quick decisions and antipatterns baked in during those weeks get copied across the codebase as more people join. By the time anyone has the bandwidth to question the original choices, they are already treated as the standard way of doing things. In practice, the senior Flutter engineer you bring in for the PoC is making architectural decisions that the whole organization will inherit.
Platform team
Once you're past the PoC and operating at scale (multiple product teams), we strongly recommend standing up a Flutter, or joint native-Flutter, platform team. Add-to-app generates a lot of shared infrastructure: engine lifecycle management, the navigation library, the state-sharing layer, contract conventions, build setup, and the documentation around all of it. If every product team owns its own slice of this, you can easily end up with several subtly different versions of each pattern. A small platform team owning the framework keeps the patterns coherent, speeds up onboarding for product teams, and gives you one place to make engine-management decisions that span the whole app.
Upskilling native developers vs hiring Flutter specialists
The hiring-vs-upskilling decision interacts with the team shape above and sets the long-term cost of the migration. If it goes wrong, you either burn out the native team trying to learn Flutter on a deadline, or you spend a long time building a Flutter team that doesn't understand your app. We've seen three patterns:
- Upskilling the native team - the lowest hiring cost, but also the steepest learning curve. It works best when the timeline is generous and the team has bandwidth.
- Upskilling plus at least one senior Flutter hire (or partner) - our recommended default. The senior engineer anchors the architecture while the native devs ramp up, and you keep the deep knowledge of the existing app. We also highly recommend running a series of internal workshops to get the native team up to speed on Flutter.
- Pure hiring - the fastest way to spin up, but you bring in a team with zero context on the app. This tends to show up in the boundary code, unless you invest heavily in shared understanding.
Who owns the boundary code?
Boundary code - the shared auth layer, the boundary contracts (Pigeon definitions, or inlay_gen schemas with Inlay), the bindings to existing native modules - is where most cross-functional friction lives. The contracts can be tailored to Flutter's needs, but they tend to lean on existing native infrastructure, and that infrastructure keeps getting changed by native teams who don't know how the Flutter side uses it. It happens in the other direction too: Flutter teams tweak some native code to suit themselves and break something on the native side. Either way, a small mistake on one side surfaces as a runtime crash on the other. In our experience, who owns this code largely determines how well the boundary holds up over time.
We recommend giving the boundary a single owner - in practice, the platform team. The common mistake is to treat boundary code as a shared space that each product team edits as needed. When everyone can touch the contracts and no one owns them, the conventions drift and the cross-language breakages become routine. The platform team that owns the framework infrastructure is the natural home for the contracts: it already sets the conventions, sees all the consumers, and can review changes for impact on both sides.
That doesn't mean the platform team writes every contract. A split that works:
- The platform team owns the contract format and the shared layers - the boundary schemas (Pigeon or
inlay_gen), the code-generation setup, auth, the state-sharing infrastructure. Changes there go through them. - Product teams own their screens and the feature-specific data that crosses their slice of the boundary, proposing contract additions through the platform team's review.
- Native teams flag changes to shared native infrastructure that Flutter depends on, rather than changing it silently.
Require review from both sides on any change that crosses the boundary. A contract change, or a change to native infrastructure that a contract relies on, shouldn't merge with only native or only Flutter eyes on it. It is a cheap safeguard against breaking the other side by accident, and much cheaper to enforce as a review rule than to debug the resulting crash after the fact. Generated contracts help here: because the types come from a single schema, a breaking change shows up as a compile error instead of a silent runtime mismatch - provided both sides are built from the same generated output. Versioning covers the setups where that stops being true.
Syncing native and Flutter roadmaps
As long as the app is part native and part Flutter, the two sides ship together - one binary, one App Store release, one Play Store release. That makes roadmap coordination something you need to plan for explicitly.
The first thing to agree on is a shared release cadence. Both sides ship in the same build, so they are on the same cadence whether they plan for it or not. The teams that struggle are usually the ones that plan independently and discover the coupling only at release time - a half-finished Flutter migration blocking a native release, or a native refactor landing mid-sprint and breaking the boundary. Agree on the cadence up front and treat the release cutoff as shared.
Feature flags are the most reliable way to keep one side from blocking the other, because they make unfinished work invisible at runtime. As covered in Planning the migration, put every migrated screen behind a feature flag (Firebase Remote Config, LaunchDarkly, or an in-house equivalent). A Flutter screen that isn't ready simply stays off - its code ships in the binary, but no user reaches it - so an unfinished migration never holds back a release. The same flags also give you the phased rollout and instant native fallback that you'd want for de-risking anyway.
Finally, coordinate the dependencies that actually couple the two sides. Most native and Flutter work is independent and needs no synchronizing, but a few exceptions are worth tracking explicitly:
- Contract changes, which by definition need both sides to land together - sequence them so the change merges, generates, and is consumed in a known order.
- Shared native infrastructure that Flutter depends on (auth, networking, analytics), where a native-side change can break Flutter even when no contract changed.
- The boundary screens themselves, where a native flow hands off to Flutter or back - a change to either side's entry or exit point needs the other side to expect it.
Keep these in a shared view that both sides can see - whatever planning tool you already use is fine - so that the coupling is visible well before the release date.