packages/docs/apps/mobile.md
The Eliza mobile app brings the full dashboard experience to iOS and Android devices using Capacitor, a cross-platform native runtime. The same web UI runs inside a native WebView with access to device hardware through Capacitor plugins.
iOS App Store and Google Play builds are Cloud-backed mobile clients. They do not run a local Bun backend, local shell, PTY-spawned Claude/Codex/OpenCode, or the privileged AOSP agent service in-app. Local generated applets run through the mobile-safe runtime and virtual file system only. JSCore, QuickJS, Android isolated-process, and AVF providers are advertised only when an actual native boundary is attached; otherwise the app falls back to VFS/WASM-safe surfaces and Cloud containers.
iOS local development and sideload builds are a separate target, not an
enterprise distribution path. Use bun run build:ios:local from
packages/app to bake runtimeMode=local, build
packages/agent/dist-mobile-ios/agent-bundle.js, stage it under
App/public/agent/, and include the native llama bridge. The default local
target is the iOS simulator; use bun run build:ios:local:device or set
ELIZA_IOS_BUILD_DESTINATION='generic/platform=iOS' plus normal Xcode signing
when you want a sideload/device build.
That target still does not imply a host shell or downloaded native code. The
full Bun engine path is gated by ELIZA_IOS_FULL_BUN_ENGINE=1 and requires
packages/native/bun-runtime/artifacts/ElizaBunEngine.xcframework (or
ELIZA_IOS_BUN_ENGINE_XCFRAMEWORK; external override paths are validated and
staged into packages/native/bun-runtime/artifacts/ before CocoaPods runs. If that
artifact is missing, the build fails instead of falling back to the JSContext
compatibility host. When the framework is present, the React app routes
local-agent requests through
Capacitor ElizaBunRuntime.call("http_request"), the native C ABI, and the
agent bundle's ios-bridge --stdio command. The WebView does not open a TCP
connection to the backend. Full-Bun iOS builds route foreground local-agent
requests through bun-host-ipc; compatibility builds can still route the
foreground local-agent URL through the in-process ITTP kernel. The kernel exposes
GET /api/local-agent/capabilities so the app can show the truth about what is
local today. Background runner JSContexts do not own the foreground native
runtime bridge, so local iOS wakes are recorded as explicit
ios_bun_host_ipc_unavailable_in_background_jscontext or
ios_ittp_route_kernel_unavailable_in_background_jscontext skips instead of
probing a fake TCP endpoint.
The AOSP / ElizaOS Android build is a separate privileged system target. It can
stage the on-device Bun agent, /system/bin/sh, shell plugin, coding-tools
plugin, agent orchestrator, and optional AVF/Microdroid boundary when the
device image exposes those APIs. The Android template includes a reflection-only
AVF probe, but the Microdroid payload/RPC lifecycle is still AOSP-only follow-up
work. AOSP terminal and toolchain behavior is outside the app-store mobile
target and remains limited to privileged system builds.
Use bun run build:android:cloud from the repository root for a Play-store
style release AAB thin client; android-cloud-debug is only for debug APK
iteration. Use bun run build:android:system for the privileged AOSP APK. The
legacy packages/app build:android script is sideload-only and embeds the
on-device agent runtime. The cloud target strips the local agent, privileged
default-role surfaces, staged runtime assets, native runtime plugin references,
ElizaAgentService, assets/agent, disguised libeliza_ native runtime
libraries, MANAGE_APP_OPS_MODES, PACKAGE_USAGE_STATS,
MANAGE_VIRTUAL_MACHINE, and other system-only permissions, then audits the
source tree and artifact.
The android-cloud target strips ElizaAgentService, system-only permissions
such as MANAGE_APP_OPS_MODES, PACKAGE_USAGE_STATS, and
MANAGE_VIRTUAL_MACHINE, staged
assets/agent files, and libeliza_ native runtime libraries before the APK
is assembled.
| Platform | Minimum Version | Scheme | Notes |
|---|---|---|---|
| iOS | iOS 14+ (armv7) | HTTPS | Automatic content inset, mobile-preferred content mode, link preview disabled |
| Android | API 26 (Android 8.0+) | HTTPS | Input capture enabled, mixed content disabled, WebContents debugging off in production |
App ID: com.elizaai.eliza
Package name: @elizaai/app
brew install cocoapods; gem install --user-install cocoapods also works
when Ruby's user gem bin directory is on PATHAll mobile build commands run from the repository root using bun run.
# Build plugins, web assets, and sync to the iOS project
bun run build:ios
# Open the Xcode project
bun run cap:open:ios
This runs vite build to produce the dist/ web assets, then capacitor sync ios to copy them into the native iOS project and update native dependencies.
The Xcode workspace is at packages/app/ios/App/App.xcworkspace.
# Build plugins, web assets, and sync to the Android project
bun run build:android
# Open the Android Studio project
bun run cap:open:android
This runs vite build followed by capacitor sync android to copy web assets and update the Gradle project.
The Android project is at packages/app/android/.
All custom Capacitor plugins must be built before the web app can bundle them:
bun run plugin:build
This iterates through each plugin directory (gateway, swabble, camera, screencapture, canvas, desktop, location, talkmode, agent, appblocker, llama, mobile-signals, websiteblocker) and runs the build script for each.
If you have already built the web assets and only need to push changes to the native projects:
cd packages/app
# Sync all platforms
bun run cap:sync
# Sync iOS only
bun run cap:sync:ios
# Sync Android only
bun run cap:sync:android
The shared Capacitor configuration lives in capacitor.config.ts. Mobile targets use that shared config, while the desktop runtime is configured alongside the Electrobun app.
{
appId: "com.elizaai.eliza",
appName: "Eliza",
webDir: "dist",
server: {
androidScheme: "https",
iosScheme: "https",
allowNavigation: [
"localhost", "127.0.0.1",
"*.elizacloud.ai", "app.eliza.ai", "cloud.eliza.ai", "*.eliza.ai",
"rs-sdk-demo.fly.dev", "*.fly.dev",
"hyperscape.gg", "*.hyperscape.gg",
],
},
plugins: {
Keyboard: { resize: "body", resizeOnFullScreen: true },
},
ios: {
contentInset: "automatic",
preferredContentMode: "mobile",
backgroundColor: "#0a0a0a",
allowsLinkPreview: false,
},
android: {
backgroundColor: "#0a0a0a",
allowMixedContent: false,
captureInput: true,
webContentsDebuggingEnabled: false,
},
}
| Field | Purpose |
|---|---|
webDir | Directory containing the bundled Vite output (dist) |
server.allowNavigation | Domains the WebView is allowed to navigate to (localhost, Eliza Cloud, game servers, etc.) |
server.androidScheme / iosScheme | Both set to HTTPS for secure WebView content loading |
plugins.Keyboard.resize | Body resize mode keeps the chat input visible when the keyboard opens |
ios.contentInset | Automatic insets for the notch / Dynamic Island |
ios.preferredContentMode | Mobile-optimized rendering (not desktop-style) |
ios.allowsLinkPreview | Disables long-press link previews that interfere with custom gestures |
android.captureInput | The WebView captures all input events (prevents Android back gesture conflicts) |
android.allowMixedContent | Disabled to prevent insecure HTTP resources in the HTTPS WebView |
android.webContentsDebuggingEnabled | Disabled in production for security (enable for development) |
The mobile app uses 13 custom Eliza Capacitor plugins plus the core Haptics plugin, each providing native capabilities with web fallbacks.
@elizaos/capacitor-gateway)Connects the mobile app to a Eliza agent running elsewhere on the network.
_eliza-gw._tcp services on the local network. Supports both local discovery and wide-area DNS-SD (e.g., over Tailscale).gatewayEvent, stateChange, error, and discovery events.@elizaos/capacitor-swabble)Voice wake-word detection for hands-free activation.
["eliza"]) with post-trigger gap detection and minimum command length.SpeechRecognition / webkitSpeechRecognition) if available.@elizaos/capacitor-talkmode)Full speech pipeline: speech-to-text, chat with agent, text-to-speech response.
idle -> listening -> processing -> speaking with event listeners for each transition.@elizaos/capacitor-camera)Full camera control with preview, photo capture, and video recording.
navigator.mediaDevices.getUserMedia.@elizaos/capacitor-location)GPS and geolocation services.
getCurrentPosition with cache age and timeout options.watchPosition with minimum distance and interval filters.@elizaos/capacitor-screencapture)Screenshot and screen recording.
getDisplayMedia.@elizaos/capacitor-canvas)Canvas rendering and web view management. Available on all platforms (HTML Canvas API is universal).
eliza:// URLs and fires deepLink events.@elizaos/capacitor-agent)Agent lifecycle management.
bun-host-ipc, with the in-process ITTP kernel kept
as the foreground compatibility path.not_started, starting, running, stopped, error).@elizaos/capacitor-desktop)Desktop-specific features (macOS/Electrobun only):
Not available on iOS/Android — these features are silently unavailable on mobile.
@elizaos/capacitor-appblocker)App blocking for focus/productivity features (LifeOps). Allows the agent to block distracting apps on the user's device.
@elizaos/capacitor-llama)On-device LLM inference via llama-cpp-capacitor. Enables local model execution without network access.
@elizaos/capacitor-mobile-signals)Mobile-specific signal handling and lifecycle events.
@elizaos/capacitor-websiteblocker)Website blocking for focus/productivity features (LifeOps). Allows the agent to block distracting websites.
@capacitor/haptics)Native haptic feedback for touch interactions (core Capacitor plugin, not custom).
The plugin bridge provides a canonical interface to all plugins with automatic platform detection and error handling.
Each plugin reports its capabilities for the current platform. The capabilities are computed at initialization time based on Capacitor.getPlatform() and web API detection:
interface PluginCapabilities {
gateway: { available, discovery, websocket }
voiceWake: { available, continuous }
talkMode: { available, elevenlabs, systemTts }
camera: { available, photo, video }
location: { available, gps, background }
screenCapture: { available, screenshot, recording }
canvas: { available }
desktop: { available, tray, shortcuts, menu }
}
Check individual features programmatically:
import { isFeatureAvailable } from "./bridge/plugin-bridge";
isFeatureAvailable("gatewayDiscovery"); // true on native
isFeatureAvailable("voiceWake"); // true on native or with Web Speech API
isFeatureAvailable("talkMode"); // true on native or with Web Speech API
isFeatureAvailable("elevenlabs"); // true everywhere (web API call)
isFeatureAvailable("camera"); // true on native or with getUserMedia
isFeatureAvailable("location"); // true if navigator.geolocation exists
isFeatureAvailable("backgroundLocation"); // true on iOS/Android only
isFeatureAvailable("screenCapture"); // true on native or with getDisplayMedia
isFeatureAvailable("desktopTray"); // true on Electrobun only
Every plugin is wrapped in a Proxy that catches and logs errors from any method call. The wrapper interface exposes:
interface WrappedPlugin<T> {
plugin: T; // The actual plugin instance
isNative: boolean; // Whether the native implementation is available
hasFallback: boolean; // Whether a web fallback exists
}
When a native plugin is unavailable, the bridge provides graceful degradation:
getUserMedia.SpeechRecognition / webkitSpeechRecognition).getDisplayMedia.hasFallback: false).Web API detection helpers check for SpeechRecognition, speechSynthesis, mediaDevices, geolocation, and getDisplayMedia before reporting capability.
On mobile, the agent typically runs on a separate machine (desktop or server). The mobile app connects to it via the Gateway plugin:
_eliza-gw._tcp services. On iOS, the NSBonjourServices and NSLocalNetworkUsageDescription keys in Info.plist authorize this. Results stream in via the discovery event as gateways are found, lost, or updated.wss://192.168.1.100:8080).On Android, the GatewayConnectionService keeps the process alive while the app is in the background. This is a foreground service with type dataSync that displays a persistent notification showing the gateway connection status.
Key behaviors:
MainActivity.onCreate() runs.isFinishing() check in onDestroy).ACTION_STOP to the service.START_STICKY so Android restarts the service if the system kills it.POST_NOTIFICATIONS permission at runtime for the notification to be visible.FOREGROUND_SERVICE_TYPE_DATA_SYNC when calling startForeground().The storage bridge ensures persistent data survives across app sessions on native platforms.
localStorage — no special handling needed.localStorage operations via a proxy on setItem, getItem, and removeItem. Syncs specific keys to Capacitor's Preferences plugin for reliable persistence. An in-memory cache (preferencesCache) is loaded from Preferences at initialization to avoid async reads during synchronous getItem calls.On native platforms, initializeStorageBridge() must be called before the app starts reading storage. It loads all synced keys from Capacitor Preferences into the cache and writes them to localStorage for immediate availability, then installs the localStorage proxy.
The following keys are automatically synced to native Preferences:
| Key | Purpose |
|---|---|
eliza.control.settings.v1 | Dashboard settings and preferences |
eliza.device.identity | Device identity token |
eliza.device.auth | Device authentication credentials |
// Read a value (works on both native and web)
const value = await getStorageValue("eliza.device.identity");
// Write a value
await setStorageValue("eliza.control.settings.v1", jsonString);
// Remove a value
await removeStorageValue("eliza.device.auth");
// Register additional keys for native sync
registerSyncedKey("my.custom.key");
// Check if initialization is complete
isStorageBridgeInitialized(); // boolean
The global bridge object is exposed on window.Eliza and provides a canonical API for all native capabilities.
| Property | Type | Description |
|---|---|---|
capabilities | CapacitorCapabilities | Platform capability flags (native, haptics, camera, microphone, screenCapture, fileSystem, notifications, geolocation, background, voiceWake) |
pluginCapabilities | PluginCapabilities | Per-plugin capability details (see above) |
haptics | object | Haptic feedback functions: light(), medium(), heavy(), success(), warning(), error(), selectionStart(), selectionChanged(), selectionEnd() |
plugins | ElizaPlugins | Access to all Eliza plugins with fallback support |
isFeatureAvailable(feature) | function | Check if a specific feature is available on the current platform |
platform | object | Platform detection: name, isNative, isIOS, isAndroid, isDesktop, isWeb, isMacOS |
getPlugin(name) | function | Get a registered plugin by name |
hasPlugin(name) | function | Check if a plugin is registered |
registerPlugin(name, plugin) | function | Register a custom plugin at runtime |
The bridge dispatches a eliza:bridge-ready custom event on document when initialization completes. Use waitForBridge() to await initialization:
import { waitForBridge } from "./bridge/capacitor-bridge";
const bridge = await waitForBridge();
console.log(bridge.platform.isIOS); // true on iPhone/iPad
If window.Eliza is already set, waitForBridge() resolves immediately. Otherwise it listens for the custom event.
The iOS app declares the following usage descriptions in Info.plist:
| Key | Description shown to user |
|---|---|
NSCameraUsageDescription | "Eliza uses your camera to capture photos and video when you ask it to." |
NSMicrophoneUsageDescription | "Eliza needs microphone access for voice wake, talk mode, and video capture." |
NSLocationWhenInUseUsageDescription | "Eliza uses your location to provide location-aware responses when you allow it." |
NSLocationAlwaysAndWhenInUseUsageDescription | "Eliza can share your location in the background so it stays up to date even when the app is not in use." |
NSSpeechRecognitionUsageDescription | "Eliza uses on-device speech recognition to listen for voice commands and wake words." |
NSPhotoLibraryUsageDescription | "Eliza accesses your photo library to attach and share photos or videos." |
NSPhotoLibraryAddUsageDescription | "Eliza saves captured photos and videos to your photo library." |
NSLocalNetworkUsageDescription | "Eliza discovers and connects to your Eliza gateway on the local network." |
NSBonjourServices | _eliza-gw._tcp (for gateway discovery) |
The iOS AppDelegate.swift handles URL opens and Universal Links via ApplicationDelegateProxy.shared, which allows the Capacitor App plugin to track deep link opens.
The Android manifest declares these permissions:
| Permission | Purpose |
|---|---|
INTERNET | Network access for gateway WebSocket and API calls |
RECORD_AUDIO | Microphone for voice wake and talk mode |
CAMERA | Photo and video capture |
ACCESS_FINE_LOCATION | GPS-based location |
ACCESS_COARSE_LOCATION | Network-based location |
ACCESS_BACKGROUND_LOCATION | Location updates while app is backgrounded |
FOREGROUND_SERVICE | Gateway connection foreground service |
FOREGROUND_SERVICE_DATA_SYNC | Typed foreground service (API 34+) |
POST_NOTIFICATIONS | Notification display (API 33+ runtime permission) |
WRITE_EXTERNAL_STORAGE | File access (API 28 and below only) |
READ_EXTERNAL_STORAGE | File access (API 32 and below only) |
WAKE_LOCK | Keep CPU awake during background operations |
| Property | Value |
|---|---|
minSdkVersion | 26 (Android 8.0) |
compileSdkVersion | 35 |
targetSdkVersion | 35 |
applicationId | com.elizaai.eliza |
namespace | ai.eliza.app |
The main activity uses singleTask launch mode, which ensures only one instance exists. It handles orientation changes, keyboard events, screen size changes, and locale changes without recreating the activity.
For rapid development with live reload (all commands from the repo root):
# Build plugins and web assets
bun run build:ios
# Start Vite dev server in a separate terminal
bun run dev
# Open Xcode and run on a simulator
bun run cap:open:ios
Update the Capacitor server config to point to your dev server IP for live reload.
# Build plugins and web assets
bun run build:android
# Start Vite dev server in a separate terminal
bun run dev
# Open Android Studio and run on an emulator
bun run cap:open:android
# Unit tests (Vitest)
bun run test
# Watch mode
bun run test:watch
Open the Xcode project, select the App target, go to Signing & Capabilities, and select your development team. For simulator-only testing, automatic signing with a personal team is sufficient.
ANDROID_HOME or ANDROID_SDK_ROOT environment variable is set.cd android && ./gradlew clean and then rebuild.Run the build command from the repo root, which includes the sync step automatically:
bun run build:ios # or build:android
_eliza-gw._tcp service type.On Android 13+, the POST_NOTIFICATIONS permission must be granted. The app requests this on first launch, but if denied, the foreground service notification is silently suppressed. Go to Settings -> Apps -> Eliza -> Notifications and enable notifications.
Haptic feedback is only available on physical iOS and Android devices. Simulators and emulators do not produce haptic output. On web, haptic calls are silently ignored.