One React Codebase, Three Platforms: Web, iOS and Android

By ·

How I ship every product from a single React codebase with Capacitor — the setup, the real platform quirks, and why this multiplies what vibe coding gets you.

Every product I ship — TaskMondo, HeadToHead, Dolphin Beyond, Runish — is one React codebase that becomes a website, an iOS app and an Android app. Not three codebases in a trenchcoat: one Vite project, wrapped with Capacitor.

For a solo developer this isn’t a preference, it’s the business model. And it quietly multiplies vibe coding: every AI-generated feature lands on three platforms for the price of one prompt. It’s also how three platforms learn to complain at the same time, but let’s start positive.

The setup

  • React + TypeScript + Vite — the app itself, exactly as if it were web-only.
  • Tailwind — with touch targets and safe areas treated as first-class, not afterthoughts.
  • Capacitor — wraps the built web app in a native shell and exposes native APIs (GPS, haptics, push, purchases) through plugins.
  • Firebase — auth, data and functions, identical across platforms.

The result: npm run build produces the website; npx cap sync plus Xcode/Android Studio produces the store builds. Same components, same state, same bugs. Ideally.

What actually bites (and what to tell the AI)

The promise is “write once, run everywhere.” The truth is “write once, debug everywhere.” The quirks are predictable, though — and predictable quirks can be encoded into your rules file so the AI stops regenerating them:

  • Safe areas. iOS notches and Android gesture bars will eat your UI. Every screen gets env(safe-area-inset-*) padding from day one. The AI won’t add it unless your rules demand it.
  • Keyboards. The on-screen keyboard resizes viewports differently on iOS and Android. Any form near the bottom of the screen needs testing on both, always.
  • Touch vs hover. Generated UI loves hover states; phones don’t hover. My rule file bans hover-only affordances.
  • The 300ms mindset. Native apps respond instantly. Web-feeling delays that are fine in a browser feel broken in an app — so optimistic UI and instant feedback are conventions, not polish.
  • Background behaviour. GPS in Runish taught me that iOS and Android disagree about what an app may do while closed, roughly the way two toddlers disagree about a toy.

None of these are hard individually. Collectively they’re the difference between “works in the simulator” and four-and-a-half stars.

Why this pairs so well with vibe coding

Vibe coding’s economics are per-feature: describe, plan, generate, review. If each feature ships to three platforms, your leverage triples without your workload changing. The AI doesn’t care that the drawer component it just wrote will be swiped on an iPhone in Aarhus and clicked in a browser in Copenhagen — but your revenue does.

The compounding effect is real: the conventions above started as bugs, became prompt lines, and now new features come out platform-correct on the first draft. That’s the guardrails-compound principle doing its quiet work.

When you’d choose differently

Native-first games with heavy 3D might outgrow the web view (Dolphin Beyond hasn’t — R3F performs, with care). Apps built entirely around one platform’s exclusive APIs deserve native code. And if a team of specialists is already paying for three codebases, fine. But for one person shipping eight products? One codebase, three platforms, no regrets — and no half-morning lost to writing the same modal twice.