AbsoluteJS

Expo

For teams that need specific screens in React Native. Set engine: 'expo', list the routes you want to rebuild natively, and every other page keeps working exactly as it does in the Capacitor app.

#Why Expo

Capacitor renders every screen in a WebView, which is the right default. Expo earns its place when a screen needs what only native views can do:

Very long listsReact Native lists recycle native views, so feeds and catalogues with thousands of rows scroll at native frame rates.
Gesture-heavy screensSwipe-to-dismiss, drag to reorder, pinch and pan run on the native gesture system instead of DOM touch events.
Native navigationEvery route sits in Expo Router’s native stack, with the platform’s own push and pop transitions and edge swipe back.
React Native librariesMaps, camera views, video and other components that only exist as React Native libraries become available on the screens that use them.
Fast RefreshNative routes reload with Metro Fast Refresh, keeping component state, while your web routes keep AbsoluteJS hot reload.

If none of these apply to your app, stay with Capacitor.

#Trade-offs

FeatureCapacitorExpo
Native project
Expo’s project is regenerated from your config on every sync; change absolute.config.ts, not the generated files.
mobile/, yours to edit.absolutejs/mobile/expo, generated
Screens in React NativeRoutes you list in routes.native
Native route framework—React Native only
Every other pageYour pageYour page, in a WebView
Device features, Auth, Sync, push
Over-the-air updates

#How it works

1
Expo Router owns navigation
The generated app is an Expo Router app. Every URL, including deep links, resolves through it, so navigation is native.
2
Native routes render React Native
Routes listed in routes.native render your React Native module, with live props from the same server handler that serves that page on the web.
3
Web routes render your page
Every other route renders your AbsoluteJS page, packaged into the app, in a WebView. Android Back walks its history before leaving the screen.
4
A bridge connects them
Pages reach device features, HTTP, sign-in and Sync through a bridge to the native side. Tokens and the local database never enter the WebView.

Web routes are not converted to React Native. A React, Svelte, Vue, Angular, HTML or HTMX page is packaged and rendered exactly as in the Capacitor app. Only the modules you list use React Native.

#Configure

Everything else in mobile works the same for both engines. routes.native maps a path pattern to a React Native module, relative to your project root. The generated app uses Expo SDK 57.

TS
import { defineConfig } from '@absolutejs/absolute';

export default defineConfig({
  mobile: {
    engine: 'expo',
    appId: 'com.example.shop',
    appName: 'Shop',
    server: { productionOrigin: 'https://shop.example.com' },
    routes: {
      native: {
        '/scanner': './mobile/native/scanner.tsx',
        '/products/:productId': './mobile/native/product.tsx',
        '/files/*': './mobile/native/files.tsx'
      }
    }
  }
});
PatternRule
/scannerA static path
/products/:productIdA named parameter matches one segment
/files/*A final * matches one or more segments, as params.absoluteWildcard
Not allowedQuery strings, fragments, trailing slashes, a root /*, repeated parameter names, two patterns that match the same URLs
Reserved/__absolute/native and first segments such as _expo, assets, public and manifest

#Write a native route

A native route is a React Native component whose props come from your existing server route. You do not write an API for it: the handler that renders the page on the web returns the same props to the app.

TS
// The same handler serves the web page and the native route's pageProps
.get('/products/:productId', async ({ params, request }) =>
  handleReactPageRequest({
    index: asset(manifest, 'ProductIndex'),
    Page: Product,
    props: await loadProduct(params.productId),
    request
  })
)
TSX
// mobile/native/product.tsx
import { FlatList, Pressable, Text } from 'react-native';
import type { AbsoluteNativeRouteProps } from '@absolutejs/absolute/mobile';
import type { ProductPageProps } from '../../src/frontend/pages/Product';

export default function Product({
  pageProps,
  params,
  reload
}: AbsoluteNativeRouteProps<ProductPageProps, { productId: string }>) {
  return (
    <FlatList
      data={pageProps.variants}
      keyExtractor={(variant) => variant.id}
      ListHeaderComponent={<Text>{pageProps.name} #{params.productId}</Text>}
      onRefresh={reload}
      refreshing={false}
      renderItem={({ item }) => (
        <Pressable>
          <Text>{item.label}</Text>
        </Pressable>
      )}
    />
  );
}
pagePropsThe props your server route produced for the current URL, typed with the page’s own props type.
paramsPath and query parameters from Expo Router.
reload()Runs the server route again and updates pageProps without remounting the screen.

#Auth, Sync and devices

Your code does not change. Native routes and web routes both use @absolutejs/auth/client, @absolutejs/sync and @absolutejs/devices; the Expo app installs these implementations for you, as soon as your package.json lists the package.

Push uses the device's APNs or FCM token directly and registers it with your server exactly as in the Capacitor app; see Push notifications.

#The bridge

Web routes call a fixed list of native methods: device features, HTTP, sign-in and Sync. Nothing else on the native side is reachable from a page.

LimitValue
Message size64 KiB
HTTP request and response bodies48 KiB
Files (photos, documents)64 MiB, transferred in 24 KiB chunks
Sync socket frames4 MiB
Requests30 seconds; pickers and permission prompts 5 minutes
HTTPProduction origin only; your own Authorization headers and redirects are refused

Sync over the bridge uses the default JSON serializer.

#Development

bun dev generates the Expo project, offers to install the pinned SDK and Android toolchain, and starts Metro, the development client and your server together. It rebuilds the native app when your config, package.json or lockfile changes.

BASH
bun dev                            # Metro, the dev client and your server together
bun dev --android-device emulator-5554
bun dev --ios-device "iPhone 16"

iOS needs macOS, or a Mac paired with absolute mobile pair mac. Development covers devices, HTTPS and the remote Mac.

#Release and updates

Store builds, the release doctor, certification and publishing use the same commands as Capacitor. Each build regenerates the native project from scratch, so nothing left over from development reaches the store.

BASH
bunx absolute mobile build android src/backend/server.ts
bunx absolute mobile build ios src/backend/server.ts
bunx absolute mobile publish android --play-track internal

Over-the-air updates use the standard expo-updates client against your own AbsoluteJS server, with an RSA code-signing certificate built into the app. Over-the-air updates covers keys and rollout.

No EAS needed
AbsoluteJS builds, signs and serves Expo apps and updates itself. EAS Build and EAS Update are not used, and you do not need an Expo account.