---
title: "Expo"
description: "Use Expo for MainSDK push notifications and over-the-air app updates - EAS project, push credentials, main.yaml, testing and swapping."
section: "integrations"
canonical: "https://mainsdk.dev/docs/integrations/expo"
updated: "2026-10-28"
---

# Expo

> Use Expo for MainSDK push notifications and over-the-air app updates - EAS project, push credentials, main.yaml, testing and swapping.

Canonical: https://mainsdk.dev/docs/integrations/expo · Updated: 2026-10-28

Expo is the toolkit and cloud service behind many React Native apps, and the MainSDK Expo adapter gives those apps the same interfaces as your web app. Expo serves two MainSDK interfaces. Its push service is a driver for [`notify`](https://mainsdk.dev/docs/interfaces/notify): your server sends once, and Expo delivers through FCM on Android and APNs on iOS. Expo Updates is a driver for [`updates`](https://mainsdk.dev/docs/interfaces/updates): it ships JavaScript-only fixes over the air and tells users when a store update is required. Pick Expo when your mobile app is built with Expo, so you manage one set of push credentials and one update channel.

## Interfaces it serves

### Notifications (`notify`)

- Status: Building for 0.3 · ships as a stable API
- Driver: `expo` (Expo)
- Docs: https://mainsdk.dev/docs/interfaces/notify.md

```yaml
notify:
  driver: expo
```

Swap to another provider: `whatsapp-cloud`, `bird`, `msg91`, `twilio`, `local`, `fcm`, `apns`, `resend`, `ses`, `onesignal`, `slack`, `pagerduty`, `opsgenie`, `webhook`.

### App updates and version blocking (`updates`)

- Status: Building for 0.7 · ships as a beta API
- Driver: `expo-updates` (Expo Updates). 1.1; wraps the expo-updates module by shape
- Docs: https://mainsdk.dev/docs/interfaces/updates.md

```yaml
updates:
  driver: expo-updates
```

Swap to another provider: `local`, `google-play`, `app-store`, `tauri`.

## Before you start

You need an Expo account and an app set up with EAS (Expo Application Services).

1. **Link the app to an EAS project.** Your app config holds the project ID that Expo's push and update services use.
2. **Give Expo your push credentials.** Upload your FCM credentials for Android and your APNs key for iOS to EAS, so Expo can deliver to both. You still create those in Firebase and the Apple Developer Program.
3. **Optionally require an access token for push.** Expo lets you require an access token on every push request, so a leaked push token alone can't be used to message your users. Turn it on for production.
4. **For updates:** install `expo-updates`, set up update channels (for example `preview` and `production`) and a runtime version policy, so an update only reaches builds it is compatible with. Turn on update code signing so devices accept only updates you signed.

Store the access token and any signing key in your secrets manager, never in code or the app bundle.

## Configure

Push through Expo:

```yaml title="main.yaml"
notify:
  channels: [push, whatsapp, sms, email]
  push: { drivers: [fcm, apns], via: expo }
```

How the Expo driver takes its access token is not in the spec yet [TBD]; it will be a `secret:` reference such as `secret:EXPO_ACCESS_TOKEN` (suggested name). A literal key fails validation with `MAIN-CONFIG-0004`.

Updates:

```yaml title="main.yaml"
updates:
  driver: expo-updates
```

> **Note:** `updates` has no spec yet. How you declare the minimum version, channels and staged rollouts is provisional and may change when the spec lands. The expo-adapter spec assumes Expo Updates is configured by `updates`, not by the adapter.

## Use it

Register for push in the app, then notify from the server:

```ts title="app/notifications/setup.ts"
import { notify } from "mainsdk/notify";

await notify.push.register();   // asks permission, stores the token

await notify(user, { template: "order.shipped", category: "transactional" });
```

Check for a required update on startup:

```ts title="app/startup.ts"
import { updates } from "mainsdk";

export async function onStartup() {
  const u = await updates.check();
  if (u.required) showUpdateScreen(u);   // your own screen
}
```

## Local development and testing

- **Push on a device:** a development build registers a real Expo push token, so you can send test pushes before release.
- **Updates:** publish to a `preview` channel and point a preview build at it before you publish to `production`.
- **Tests:** `createTestApp()` from `@mainsdk/testing` runs `notify` and `updates` on in-memory fakes. `chaos.on("notify", { outage: true })` simulates a push outage.

## What MainSDK adds on top

- Push only with opt-in, respecting opt-outs and quiet hours; no personal data in push payloads.
- Updates are signed, and blocking an old version is enforced by the server as well as the app.
- Blocked users can still export their data, and every change to the minimum version is audited.

See [Notifications](https://mainsdk.dev/docs/interfaces/notify) and [App updates and version blocking](https://mainsdk.dev/docs/interfaces/updates).

## Limits and notes

- The `notify:expo` driver comes with the expo-adapter spec (phase 3, milestone 1.1). `updates:expo-updates` is listed for phase 2, with no spec yet; whether it ships with the adapter or earlier is an open question in that spec.
- Over-the-air updates carry JavaScript and assets only. A change to native code needs a store build. On iOS, over-the-air changes must stay within Apple's rules.
- Where device tokens are stored is not in the spec yet.
- `updates` is free in your app; Cloud Teams adds release dashboards and Fleet adds fleet-wide version rules.

## Swap to or from it

Send push to FCM and APNs directly instead of through Expo:

```yaml title="main.yaml"
notify:
  push: { drivers: [fcm, apns] }   # was { drivers: [fcm, apns], via: expo }
```

> **Warning:** Expo push tokens are not FCM or APNs tokens. Moving off Expo push, or onto it, needs each device to register again with `notify.push.register()`, and devices running an old build keep the old token until they update. How MainSDK runs that move is [TBD]. For updates, most apps use a different driver per target rather than swapping one.

## FAQ

**Do I still need Firebase and an Apple key with Expo?** Yes. Expo delivers through FCM and APNs using credentials you upload to EAS, but your server only talks to Expo.

**Can I fix a bug without a store review?** For JavaScript-only changes, yes, with Expo Updates on Android, and on iOS where Apple's rules allow. Native changes need a store release.

**Does Expo Updates block old versions by itself?** No. MainSDK enforces the minimum version on the server too, so a modified client can't skip it.

## Next

- [Notifications](https://mainsdk.dev/docs/interfaces/notify) and [App updates and version blocking](https://mainsdk.dev/docs/interfaces/updates)
- [Firebase Cloud Messaging](https://mainsdk.dev/docs/integrations/firebase-cloud-messaging), [Apple Push Notification service](https://mainsdk.dev/docs/integrations/apns), [App Store](https://mainsdk.dev/docs/integrations/app-store) and [Google Play](https://mainsdk.dev/docs/integrations/google-play)
- [Swap a driver](https://mainsdk.dev/docs/guides/swap-a-driver)
