Shipping a mobile app used to mean a team: an iOS specialist, an Android specialist, a backend crew, a designer, a QA pass, and someone whose entire job was wrangling provisioning profiles. We ship native apps on both platforms with a fraction of that. Not by cutting corners — by making a few decisions early that remove the need for most of the headcount.
Here is how we actually do it.
Native on both platforms — and we don't apologize for it
We could write once and ship everywhere with a cross-platform framework. We mostly don't. We build native iOS in SwiftUI and native Android in Kotlin with Jetpack Compose.
The reason is that the two ecosystems converged on the same idea at the same time: declarative, state-driven UI. SwiftUI and Compose are conceptually almost the same framework. You describe what the screen should look like for a given state, the framework reconciles the rest. An engineer fluent in one reads the other with a day of ramp-up. So "two native apps" no longer means "two completely separate skill sets" — it means the same mental model expressed in two closely related languages.
What we get for that: true platform-native feel, full access to every OS capability the day it ships, no abstraction layer between us and a bug, and binaries that behave exactly the way each platform's users expect. For apps where the experience is the product, that is worth the modest duplication.
The shared design system does the heavy lifting
The thing that actually keeps two native codebases from doubling our work is a single shared design system that lives upstream of both.
- One set of design tokens — colors, spacing, type scale, radii, motion — defined once as the source of truth.
- Those tokens generated into Swift for iOS and Kotlin for Android, so a brand color is changed in one place and both apps update.
- A shared catalog of component specs — what a primary button is, how a card behaves, what an empty state looks like — so the two apps stay visually identical without a human eyeballing them side by side.
The payoff is that "design" is not re-litigated per platform. We decide once, encode it once, and both apps inherit it. A designer and one or two engineers can hold the entire visual language in their heads.
The trick to lean mobile is not writing less code. It is making the same decision once and letting both platforms consume it.
Architecture we reuse on every app
We do not reinvent the structure each time. Both platforms get the same shape:
- A clear UI layer (SwiftUI / Compose) that only renders state.
- A state layer — observable models on iOS, ViewModels on Android — that holds logic and survives configuration changes.
- A data layer with a repository pattern in front of the network and local storage, so the UI never talks to an API directly.
- A shared backend (often the same Supabase or API we would build for the web), so business logic lives server-side once and both apps are thin clients over it.
Because the architecture is identical across projects, starting a new app is mostly assembly, not invention. Most of the foundation — the boring stuff that has to be right — already exists.
Keeping it shippable: the unglamorous disciplines
Lean only works if quality does not collapse. The non-negotiables:
- Continuous builds from day one. Every change produces an installable build for both platforms. No "integration week" at the end where everything is glued together and nothing works.
- TestFlight and Play internal testing early. Real builds on real devices in real hands within the first couple of weeks. Simulators lie about performance and never show you the things that actually break.
- Crash and analytics wiring before launch, not after. You cannot fix what you cannot see. The instrumentation goes in while the app is still small.
- A release checklist, not release heroics. App icons, splash screens, permission strings, privacy declarations — all on a list, because the boring metadata is exactly what gets you rejected.
The submission process, demystified
This is where teams lose a week to confusion. It does not have to.
Apple — App Store Connect. Provisioning and signing are the historical pain; modern Xcode's automatic signing removes most of it. The real work is the listing: screenshots at the required sizes, an accurate privacy nutrition label, age rating, and a description that survives review. App Review is stricter and slower than Google's — typically a day or two — and it rejects on guideline details, so read the relevant guidelines before you submit, not after the rejection email.
Google — Play Console. Faster to get moving, but Google front-loads the bureaucracy: a data safety form, content rating questionnaire, target API level requirements, and a staged rollout you should actually use. Push to a small percentage first, watch the crash-free rate, then ramp. Newer developer accounts also face a closed-testing requirement before production access — plan for it so it does not surprise you at the finish line.
The meta-lesson: store submission is a documentation exercise, not an engineering one. Treat the listing, the privacy disclosures, and the metadata as a first-class deliverable with its own checklist, and review stops being a lottery.
What "lean" really buys you
Shipping mobile without a ten-person team is not about working ten times harder. It is about a handful of upstream decisions:
- Native on both platforms, accepting that SwiftUI and Compose are close enough to share one mental model.
- A single design system that both apps consume, so visual decisions happen once.
- A repeatable architecture, so new apps are assembled rather than invented.
- A shared backend, so logic is not duplicated three times.
- A submission checklist, so the stores are a process and not a panic.
Get those right and a very small team ships real, native apps to both stores — repeatedly, and without the heroics. That is the entire game: decide once, reuse relentlessly, and keep the boring parts on a checklist.