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 APKapex mobile android --assemble produces 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.

In-memory + snapshot, not a native store

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.

PlatformEngineStatus
Androidandroidx.javascriptengineTurnkey — apex mobile android --assemble builds an installable APK.
iOS SimulatorJavaScriptCore (WKWebView)apex mobile ios scaffolds the shell; engine CI-verified on the Simulator.
iOS deviceJavaScriptCore (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 isIt 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.
Where external services fit

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.