Mobile app (Expo)

An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api.

An Expo (React Native) app that consumes your Voltro api as a third client — alongside the web frontend — with the same typed hooks you already use. Template id: mobile-app (kind: mobile).

Expo owns the dev loop, not voltro dev. A mobile app is started with expo start / expo run:ios, not voltro dev. The sibling api still runs under voltro dev; the app connects to it over the network. That is why a mobile app gets no port and is not part of voltro dev's orchestration.

Scaffold

# A project with a backend + a mobile app:
voltro create-project acme --api=api-backend --mobile

# …or add one to an existing project later:
voltro add-app mobile --template=mobile-app --to acme

What ships

mobile-app/
  app.config.ts            # EXPO config (scheme for deep links, plugins)
  metro.config.js          # monorepo watchFolders so @voltro/* resolve
  voltro.mobile.ts         # which api(s) this app consumes
  src/
    client.ts              # the Voltro client + the M0 seam (read its header)
    persistence.ts         # RN key-value with a secure/plain split
    lib/
      deeplinks.ts         # the typed deep-link table + pure resolver  (tested)
      sync.ts              # the background-sync decision layer          (tested)
      notifications.ts     # build a _voltro_devices row from a token    (tested)
    app/                   # Expo Router screens (list · detail · settings)
  tests/                   # pure-logic tests (no simulator needed)

The reactive loop, on a phone

The screens use the same hooks as the web frontenduseSubscription('app', 'notes.list') streams the server query; useMutation('app', 'notes.create') writes it; a server delta re-renders the list. No refetch, no polling. Offline-first is the default: offlineFirstDefaults + an RN persistence adapter mean the app opens offline, queues mutations, and reconciles on reconnect, with a first-class connection banner.

Deep links are typed and go through one table: a universal link, a custom-scheme URL, and a push payload's data.deepLink all resolve via the same matchFirstDeepLink path.

Honest status — read before you expect the loop on a device

The pure logic (src/lib/*) is real and unit-tested (pnpm test, no simulator), and the client wiring now exists: src/client.ts calls @voltro/client's buildApiRuntime with RN's globalThis.WebSocket, so buildApiHandle() returns a live handle. What it still waits on is codegen emitting this app's rpc group + descriptor map (on web those come from the generated entry) — until that lands, buildApiHandles() returns an empty map and the screens show their loading state, deliberately rather than faking one. Its header spells out the remaining step.

Booting the app on a device (expo run:ios) is your Expo/EAS CI step — there is no iOS/Android toolchain in the framework repo.

Prefer pure-native (SwiftUI / Compose)?

You do not need React Native. voltro build api --target swift (or --target kotlin) emits a typed native SDK package from your api — see React Native & mobile.