Mobile
Run on the device.
apex build --mobile packages an Apex app into a self-contained bundle that runs your
full SSR + API pipeline on a bare on-device JS engine — offline, no server, no port.
The same <script server> loaders, /api routes, and auth from the web
build, shipped inside an installable app.
What it is #
Most ways to put a web app on a phone either ship a thin shell around a hosted site (which needs a
network) or rebuild the UI in native widgets. Apex does neither. apex build --mobile takes
the same app you'd deploy to a server — pages, API routes, database, auth — and runs its
whole request pipeline on the device, with no server and no network required.
The bundle runs on a bare on-device JavaScript engine — Android's
androidx.javascriptengine, iOS's JavaScriptCore. Inside the app you get
server-rendered pages, your /api routes, an on-device SQLite database
(seeded at boot, persisted across cold starts), and sealed-cookie auth — all
offline. It's the same <script server> loaders, the same routes, the same auth as
the web build, unchanged.
- Offline, full-stack — SSR + API + DB + auth run locally; no localhost server, no port.
- One codebase — the web build and the mobile build are the same TypeScript app.
- One command to an APK —
apex mobile android --assembleproduces an installable Android package.
Quickstart #
Build the self-contained mobile bundle, then scaffold and assemble the Android shell around it.
apex build --mobile # → dist/mobile/server.mjs (self-contained, ~3.2 MB)
apex mobile android # scaffold mobile/android + sync the bundle as an asset
apex mobile android --assemble # also run gradle assembleDebug → an installable APK
Name the app and set its package id when you scaffold:
apex mobile android --appId com.you.app --name "My App" --icon icon.png --assemble
--assemble shells out to Gradle, so it needs the Android SDK installed. Without it, the
command still scaffolds mobile/android and syncs assets — open it in Android Studio to
build. Full flag reference on CLI → apex mobile.
Targeting iOS? The same bundle drives a WKWebView shell:
apex mobile ios # scaffold mobile/ios + sync the bundle & client assets
apex mobile ios --appId com.you.app --name "My App" --generate # + run xcodegen generate
apex mobile ios scaffolds on any OS; --generate runs
xcodegen generate to produce the Xcode project. Building and signing needs a Mac + Xcode —
see iOS status below.
How it works #
The native shell embeds a JS engine and evaluates the bundled server.mjs, which sets
globalThis.APEX.run. For every WebView request, the shell calls that function in the
engine, gets back { status, headers, body }, and hands the SSR HTML to the WebView, which
renders it and then hydrates. There's no localhost server and no port — just a
function call — which also sidesteps iOS background-suspension of sockets.
┌──────────────────────────────┐
│ Native shell │ Android: WebView + androidx.javascriptengine
│ │ iOS: WKWebView + JavaScriptCore
│ ┌────────────────────────┐ │
│ │ WebView │ │
│ │ renders SSR HTML, │ │
│ │ then hydrates (Alpine)│ │
│ └───────────▲────────────┘ │
│ request │ {status, │
│ │ headers, │
│ ▼ body} │
│ ┌────────────────────────┐ │
│ │ Bare JS engine │ │
│ │ server.mjs → │ │
│ │ globalThis.APEX.run │ │ ← full SSR + /api pipeline
│ │ ├── runtime shim │ │ (Buffer/TextEncoder/URL/
│ │ ├── on-device SQLite │ │ Request/Response/fetch…)
│ │ └── sealed-cookie auth│ │
│ └────────────────────────┘ │
└──────────────────────────────┘
A bare engine isn't a browser and isn't Node, so it lacks the web globals server code expects. A
runtime shim provides them — Buffer, TextEncoder,
URL, Request, Response, fetch, and friends — so the
unchanged app code runs as-is.
On-device DB, auth & persistence #
The on-device database is SQLite via sql.js, compiled as pure JavaScript / asm.js —
not WebAssembly. The sandboxed engine can't JIT WASM, so the pure-JS build is what
runs. It's seeded at boot from your migrations, and persists across cold starts: the
database is snapshotted to a file, and restored on the next launch. Reads and writes go through the
same @apex-stack/data API as on the server.
Auth is the same sealed-cookie session mechanism as the web build — an HMAC-signed
cookie, computed without WebCrypto so it works on the bare engine. Your server/auth.ts
policy, login/logout routes, and gated endpoints behave identically on-device.
The database lives in memory while the app runs and is persisted via a snapshot file — durable across cold starts, but it's a single-file SQLite image, not a native content provider. Plan seeds and migrations accordingly.
iOS status #
apex mobile ios scaffolds a WKWebView + JavaScriptCore shell into
mobile/ios and syncs the same apex build --mobile bundle plus client assets into
mobile/ios/Generated/ — running the identical SSR + API pipeline as Android. The engine is
CI-verified on the iOS Simulator (JavaScriptCore), with the same on-device story as
Android: SSR + API + on-device SQLite + auth, offline.
apex mobile ios --appId com.you.app --name "My App" # scaffold + sync assets
apex mobile ios --generate # run xcodegen generate (no committed .xcodeproj)
open mobile/ios/ApexShell.xcodeproj # on a Mac: add a free Apple ID team, run on a device or the Simulator
The Xcode project is generated with XcodeGen, so there's no .xcodeproj
committed to the repo; --generate runs xcodegen generate for you. The command
scaffolds fully on any OS, but the compile and sign step is Mac-only
(the Xcode toolchain) — so iOS isn't yet the one-command --assemble story Android is. What
still needs a real Mac / iPhone is the device build & sign and the
WKURLSchemeHandler POST-body path. Android is the turnkey target today.
| Platform | Engine | Status |
|---|---|---|
| Android | androidx.javascriptengine | Turnkey — apex mobile android --assemble builds an installable APK. |
| iOS Simulator | JavaScriptCore (WKWebView) | apex mobile ios scaffolds the shell; engine CI-verified on the Simulator. |
| iOS device | JavaScriptCore (WKWebView) | Command scaffolds on any OS; xcodegen generate + build/sign need a Mac. |
What it is / isn't #
Being precise about the trade-off matters more than the pitch. Here's the honest framing.
| It is | It isn't |
|---|---|
| A WebView app — like Capacitor — but one that runs your server (SSR + API + DB + auth) on-device, from one TypeScript codebase. | Not React-Native-style native widgets. Your UI is HTML rendered in a WebView. |
| A real offline full-stack app — the request pipeline runs locally with no network. | Not a thin wrapper around a hosted site — there's no server or port involved. |
| Backed by an on-device SQLite that's in-memory and snapshot-persisted across cold starts. | Not a native database or content provider; it's a single-file SQLite image. |
Able to reach external APIs (Supabase, Turso, …) over HTTP from client code — <script client> runs in the WebView with real network. | Not a full native-device bridge yet — camera and other native APIs need shell wiring and are limited today. |
The on-device server pipeline is fully offline, but your <script client> code runs
in the WebView with a real network connection — so you can still call Supabase, Turso, or any HTTP
API directly from the client when the device is online. Offline-first locally, cloud when you want it.
The mobile build is the same app as your web build — see Build & deploy for the server target it mirrors, and CLI → apex mobile for every flag.