One H5 codebase, native on iOS 12: the GoBrain tech stack
How we ship a PixiJS puzzle game to iOS, Android, and the web from a single TypeScript codebase, with over-the-air updates that still reach iOS 12 devices.
GoBrain is a small brain-puzzle app: a fast 2048 plus a rotating set of daily logic games. It runs as a native app on iOS and Android and as a plain website, all from one codebase. This post walks through the stack that makes that possible, and the parts that took real work to keep old phones in the picture.
One engine, three targets
The whole game is an H5 (HTML5) app built on PixiJS v7, a lightweight WebGL renderer. We picked PixiJS over a full game engine on purpose: the puzzles are 2D, the animations are tile slides and merges, and we wanted the bundle small enough to load instantly in a mobile WebView. The game logic and UI are written in TypeScript, bundled with Vite.
That single web build is then wrapped for the stores with Capacitor 6 (the Ionic native runtime). Capacitor gives us a thin native shell around the web app plus typed bridges to the platform: haptics, local notifications, share sheets, the status bar, preferences storage, and Firebase Analytics through the @capacitor-firebase plugins. The web version and the native version run the exact same game code.
Each mini-game (2048, memory, patch, sudoku, and friends) is built as its own module and stitched together by a generated catalog, so adding a new puzzle does not touch the others. End-to-end runs go through Playwright against the built games.
Keeping iOS 12 alive
Here is the interesting constraint: GoBrain deploys down to iOS 12.0. A 2018 iPhone should still be able to play.
The catch is over-the-air updates. We ship bug fixes and new puzzles without waiting for an App Store review by pushing new web bundles with @capgo/capacitor-updater. But the stock updater assumes a modern WebView, and it breaks on the older WebKit that iOS 12 ships. So we run a pinned fork of it:
"@capgo/capacitor-updater": "github:fighttechvn/capacitor-updater#ios12-6.45.10"
That fork keeps the OTA download-and-swap flow working on the iOS 12 WebView, which means even the oldest supported devices get the same fixes as everyone else, on the same day, without a store round-trip. Being able to iterate this fast is the main reason the game lives on the web platform instead of a native engine.
Compact data over the wire
Leaderboards and the game catalog move as Protocol Buffers via @bufbuild/protobuf. Protobuf keeps payloads small and strongly typed on both ends, which matters on the slow connections that older phones often sit on. The .proto definitions are the single source of truth, and the TypeScript types are generated from them at build time.
Why this shape
The stack is a set of trade-offs around reach and iteration speed:
- Reach — one codebase covers iOS, Android, and the web, and the OS floor sits low enough (iOS 12) that a large tail of older devices is still invited in.
- Iteration speed — OTA updates mean a fix is live in minutes, not a week, even on the oldest hardware.
- Small footprint — PixiJS + Vite keep the download light, so the first game loads fast on a phone that has seen better days.
None of these are exotic technologies. The work was in the seams: forking the updater for iOS 12, splitting games into independent modules, and generating types and catalogs so the small team can move quickly without breaking the old-device promise.
If you want to see it run, GoBrain plays right in the browser with no install.