MainSDK
Search⌘K
How-to

Build a mobile app with Expo

Generate app.json, the iOS privacy manifest and the Play Data safety form from main.yaml, keep secrets in Keychain, attest the device and send push through notify.

Planned for 1.1

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will point an Expo app at MainSDK, let mainsdk sync write the native files, and add the device-side rules. It takes about 30 minutes.

You need

  • A MainSDK app and an Expo app. Push and attestation take a real device.

1. Declare the mobile target

main.yaml
targets:
  mobile:
    framework: expo
    platforms: [ios, android]
    bundleId: dev.acme.orders
    signing:
      teamId: ABCDE12345
      androidSha256: ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
notify:
  channels: [push]

Run mainsdk sync. It writes app.json, mobile/PrivacyInfo.xcprivacy, mobile/play-data-safety.csv and the two association files under public/.well-known/. mainsdk explain config --target mobile shows where each app.json value came from.

2. Keep secrets in Keychain and Keystore

app/session.ts
import { DeviceVault, type SecureStorePort } from "mainsdk/expo";

// In the app: import * as SecureStore from "expo-secure-store"; it has the three calls SecureStorePort names.
declare const SecureStore: SecureStorePort;
declare const token: string;   // the refresh token from sign-in

export const vault = new DeviceVault(SecureStore);
await vault.saveRefreshToken(token);

Wrap AsyncStorage with guardedPlainStore so a write whose key names a secret fails with MAIN-MOBILE-0002. mainsdk check raises MAIN-MOBILE-0003 for a secret written to AsyncStorage and MAIN-MOBILE-0004 for a raw fetch.

On the server, build an Attestation with a verifier for App Attest and Play Integrity, call check before an action marked attest: required, and a request with no valid token fails with MAIN-AUTH-0100. On the phone, a DeviceGate decides analytics.track and storage.write with the same policy engine as the server, so an EU device sends nothing until the person agrees.

4. Send push through notify

registerPush asks permission and stores the token only when the person says yes. The expoPush, fcmPush and apnsPush drivers carry ids and no message text, and guardPush drops any field your labels mark as personal. Quiet hours and the stop action are notify's.

Check that it works

  • mainsdk check passes, and fails with MAIN-NET-0050 when Info.plist sets NSAllowsArbitraryLoads.
  • Send a push at 23:00 in the person's time zone: it is held until 07:00.
  • The Expo, FCM and APNs request shapes follow each provider's documentation; send one push through each sandbox before launch.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet