From PWA and Web to Mobile Stores: Managing Multiple Platforms from One Codebase
One codebase should mean a shared product core, not pretending every platform behaves the same. Capabilities and release operations need explicit boundaries.
Packaging a web app as Android does not remove platform reality. Keyboards, safe areas, deep links, billing, offline startup, signing and store versioning all introduce behavior that should be contained rather than scattered through the product.
1. Separate product core from platform capabilities
export interface PlatformBridge { share(input: SharePayload): Promise<void>; openExternal(url: string): Promise<void>; getInsets(): Promise<SafeAreaInsets>; billing: BillingAdapter;}Domain logic and most UI stay shared. Native capabilities sit behind a bridge so components ask for a capability rather than branching on isAndroid throughout the tree.
2. Maintain a release matrix
For every surface I document the artifact, version source, signing owner, validation gates and rollback method. This prevents release knowledge from living only in memory.
3. Keep versioning from one source
A human-selected semantic version should drive platform build numbers. Duplicating version state across package files, Gradle and store dashboards creates drift.
4. Design for N-1 mobile clients
Web can roll back quickly; store clients remain installed for weeks. Backend contracts and schema changes should account for at least the previous active mobile version.
5. Real-device release gates
- Keyboard and modal behavior.
- Safe areas and status bar.
- Cold start and expired session recovery.
- Deep/universal links.
- Offline startup.
- Billing entitlement consistency.
- Compatibility between new web bundles and older native shells.
6. CI/CD is a release gate
Not every commit needs an expensive signed native build. Fast checks can run continuously while store artifacts are produced only on explicit release triggers. Automation should prevent wrong releases, not maximize build volume.