ADR-024: Mobile Strategy — Capacitor for Android APK
Superseded by ADR-053 (2026-08-11) — Capacitor is abandoned in favour of a dedicated native app. This record is kept for historical context only.
- Status: Superseded by ADR-053
- Date: 2026-04-01
- Depends on: ADR-001 (Global Architecture), ADR-023 (Authentication Strategy)
Context and Problem Statement
Bike Trip Planner needs a mobile version to allow consulting trips offline while bikepacking. The target device is a Samsung Galaxy S20 FE running Android, used by a single user who primarily uses Firefox (not Chrome) as their default browser.
The mobile solution must:
- Produce an installable APK — sideloaded directly onto the device, no app store publication required
- Reuse the existing Next.js codebase — avoid maintaining a separate mobile codebase
- Work independently of the system browser — the user runs Firefox, which limits browser-dependent solutions
- Support offline access — trips must be consultable without network connectivity while on the road
- Access native APIs — filesystem storage, network status detection, and potentially GPS
Usage Context
This is a private-use application with no store distribution. The APK is installed manually via sideloading. This removes all constraints related to app store review policies, bundle size limits, and update mechanisms.
Decision Drivers
- Code reuse — maximize shared code between web and mobile to minimize maintenance burden
- Browser independence — must not depend on Chrome or any specific system browser
- APK output — must produce a standalone Android application package
- Offline capability — trip data must be accessible without network
- Development effort — single-developer project; minimize additional tooling and learning curve
Considered Options
| Criteria | PWA (Firefox) | Capacitor | React Native |
|---|---|---|---|
| Produces an APK | No | Yes | Yes |
| Single codebase | Yes | Yes | No (rewrite) |
| Browser independent | No (host browser) | Yes (embedded WebView) | Yes (native) |
| Development effort | Low | Low–Medium | High |
| Native API access | Limited | Full (via plugins) | Full |
| Performance | Browser-dependent | WebView (adequate) | Native (best) |
Option A: PWA on Firefox
Progressive Web App installed via Firefox's "Add to Home Screen."
Pros:
- Zero additional tooling — the existing Next.js app already supports PWA patterns
- No build pipeline changes
- Automatic updates via service worker
Cons:
- Firefox on Android has limited PWA support — no standalone display mode, no reliable service worker lifecycle, no background sync
- Does not produce an APK — just a browser shortcut
- Offline caching is unreliable and varies across Firefox versions
- No access to native APIs beyond what the browser exposes
- Eliminated by the Firefox constraint — this approach only works well on Chrome-based browsers
Option B: Capacitor (chosen)
Ionic Capacitor wraps the Next.js frontend in a native Android WebView, producing a standard APK.
Pros:
- Reuses the entire Next.js codebase — the web app runs inside a WebView with a native shell
- Produces a standard APK installable via sideloading
- Uses Android System WebView (independent of the user's default browser)
- Full access to native APIs via Capacitor plugins (Filesystem, Network, Geolocation, Preferences)
- Active ecosystem with official and community plugins
- Low learning curve for a web developer — configuration is mostly
capacitor.config.ts
Cons:
- WebView performance is slightly lower than fully native — acceptable for a data display app
- Requires a dual build strategy (web vs. mobile output modes)
- Adds Android SDK tooling to the development environment
- Debugging requires Android Studio or Chrome DevTools remote debugging
Option C: React Native
Full rewrite of the frontend using React Native components.
Pros:
- Native performance and platform-specific UI
- Direct access to all native APIs
- Mature ecosystem with strong community support
Cons:
- Requires a complete frontend rewrite — React Native components are not compatible with React DOM/Next.js
- Two separate codebases to maintain (web + mobile)
- Massive effort for a single-developer project with a single target device
- Overkill for what is essentially a data consultation app
- No code sharing with the existing Next.js frontend beyond business logic utilities
Decision
Capacitor to wrap the existing Next.js frontend into an Android APK.
Dual Build Strategy
The Next.js application supports two output modes controlled at build time:
| Target | Next.js output |
Purpose |
|---|---|---|
| Web | 'standalone' |
Production server deployment (Docker/Node.js) |
| Mobile | 'export' |
Static HTML export for Capacitor WebView |
The output mode is toggled via an environment variable (e.g., BUILD_TARGET=mobile), keeping a single next.config.ts with conditional configuration.
Architecture Adaptations
i18n: Client-Side Only
Next.js static export (output: 'export') does not support the built-in i18n routing. All internationalization is refactored to client-side only, using a library like next-intl in client mode or a lightweight i18n solution that does not depend on server-side locale detection.
This has no impact on the web build since the application already targets a single locale.
API URL Configuration
The mobile app must communicate with the remote API server. The API base URL is parameterized via NEXT_PUBLIC_API_URL, allowing the Capacitor build to point to the production API while the web build uses relative URLs or the Docker-internal address.
CORS Extension
The Capacitor WebView serves content from capacitor://localhost on Android. Two components need updating.
1. Symfony API — CORS_ALLOW_ORIGIN regex:
The backend uses origin_regex: true with a single %env(CORS_ALLOW_ORIGIN)% entry (api/config/packages/nelmio_cors.php). No PHP config change is required; extend the regex in the environment variable to include the capacitor scheme:
# dev
CORS_ALLOW_ORIGIN='^(https?|capacitor)://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
# prod
CORS_ALLOW_ORIGIN='^(https://example\.com|capacitor://localhost)$'
2. Mercure SSE (compose.yaml):
MERCURE_EXTRA_DIRECTIVES must also allow capacitor://localhost, since SSE subscriptions for real-time trip computation events originate from the WebView:
MERCURE_EXTRA_DIRECTIVES: "cors_origins ${FRONTEND_URL:-https://localhost} capacitor://localhost"
Without this second update, real-time trip computation events will fail silently in the mobile app.
Offline Access
Capacitor's Filesystem and Preferences plugins store trip data locally on the device. When the app detects network availability (via the Network plugin), it syncs trip data from the API. When offline, the app serves cached trip data from local storage.
Consequences
Positive
- Single codebase — web and mobile share the same React components, Zustand stores, and API client
- Browser independent — the embedded WebView is decoupled from the user's Firefox installation
- Installable APK — standard Android package, sideloaded without store constraints
- Offline capable — native storage plugins provide reliable offline data access
- Low incremental effort — no frontend rewrite, mostly configuration and build pipeline changes
Negative
- Dual build complexity —
next.config.tsmust handle two output modes; CI pipeline needs a mobile build job - i18n refactoring — internationalization must move to client-side to support static export
- WebView limitations — some advanced CSS or browser APIs may behave differently in Android WebView
- Android SDK dependency — developers need Android Studio and SDK tools for mobile builds
Neutral
- No store publication — eliminates app review delays but also means no automatic update distribution (user must manually install new APKs)
- Single device target — simplifies testing (only Samsung Galaxy S20 FE) but decisions may need revisiting if more devices are added