One codebase. Web, desktop, and mobile. That is the pitch every framework makes, and it is usually a lie. Here is how I actually pulled it off at Hapax AI, and what I learned building a React Native game on the side.
The Hapax Monorepo
Hapax AI ships on three platforms: a Next.js web app, an Electron desktop app, and a React Native mobile app. All of them share code through a Turborepo monorepo. The shared packages include a UI component library, API client, custom hooks, TypeScript types, and configuration. When I fix a bug in the shared API client, it is fixed on web, desktop, and mobile in the same commit.
This is not theoretical architecture. This is a production system serving banking clients. The web app runs Next.js 16 with React 19. The desktop app runs Electron with Vite and has 40+ Playwright E2E test suites. The mobile app runs Expo 54 with React Native, NativeWind for styling, and native modules for audio streaming and document picking.
The Desktop App
The Hapax desktop app is an Electron application packaged with Electron Forge. It is not a glorified browser window. It has IPC communication between the main and renderer processes, Stripe integration for payments, Datadog observability, AWS Secrets Manager integration, and support for the Model Context Protocol (MCP) SDK. I worked on this alongside the web app, shipping features that needed to work identically on both platforms.
The tricky part about Electron is not getting it to work. It is getting it to work well. Memory management, native file system access, auto-updates, and making sure the app does not feel like a website in a frame. The E2E test suite with 40+ Playwright tests exists because desktop apps do not get the luxury of “just refresh the page.”
The Mobile App
The Hapax mobile app runs on Expo 54 with React Native. NativeWind handles styling so the Tailwind classes from the web app translate directly. It has audio streaming for voice interactions, Expo document picker for file uploads, secure storage for auth tokens, push notifications, and gesture-based navigation with Reanimated.
Mobile is a different beast. You cannot lazy load the same way. Navigation patterns are fundamentally different from web. Touch targets need to be bigger. Offline handling matters. The shared code from the monorepo gives you maybe 60-70% reuse. The other 30% is platform-specific work that you cannot shortcut.
Real-Time Collaboration
One of the features that spans all three platforms is real-time collaboration. The WebSocket server runs on Bun with Y.js for CRDT-based document sync and TipTap for collaborative rich text editing. When two users edit the same document on different platforms, their changes merge without conflicts. Y.js handles the conflict resolution, but the integration work, making it reliable across web, desktop, and mobile with different network conditions, that is where the real engineering happens.
Side Project: WaveRider
Outside of work, I built WaveRider, a React Native windsurfing arcade game for iOS. It is a completely different kind of cross-platform challenge. Game loops, animation frames, touch input handling, physics calculations, all running at 60fps on a phone. No server. No API calls. Just React Native and a lot of math. It was a good reminder that cross-platform development is not just “enterprise apps on multiple screens.” It is whatever you want to build, wherever you want it to run.
The Stack
- Web: Next.js 16, React 19, TypeScript, Tailwind CSS
- Desktop: Electron, Vite, Electron Forge, Playwright
- Mobile: React Native 0.81, Expo 54, NativeWind, Reanimated
- Shared: Turborepo monorepo, shared UI/hooks/types/API client packages
- Real-time: Bun WebSocket server, Y.js, TipTap, PartyKit
- Testing: Vitest for unit, Playwright for E2E across platforms
What I Learned
Cross-platform development is not about writing code once and deploying everywhere. It is about sharing the right code and writing platform-specific code where it matters. The Turborepo monorepo at Hapax lets us share business logic, types, and API clients. But each platform has its own navigation, its own rendering quirks, its own performance characteristics. The skill is knowing what to share and what to specialize.
Also, if you are building a monorepo with web, desktop, and mobile apps, use Bun. The install times alone will change your life.