An Expo app needs a backend for four jobs that the app cannot do for itself: verify who the user is, decide what they paid for, send them push notifications, and hold any secret. The app runs on the user’s device, so anything inside its JavaScript bundle is readable. Your server should verify the sign-in token, check purchases with the store, keep push tokens in a table, and call third-party services with keys the app never sees.
This article walks through those four jobs for an Expo project, with links to the official Expo, Apple, Google, and Firebase documentation for each step.
Why can’t an Expo app keep secrets or make the final decision?
Expo apps are JavaScript running on the device. Expo documents that variables prefixed with EXPO_PUBLIC_ are inlined into the JavaScript bundle at build time, and tells you not to put sensitive values in them. See Expo’s guide to environment variables. React Native’s own security guidance says the same about storing secrets in app code: a key shipped in the app can be extracted.
That has two consequences for your design.
Secrets live on the server. A model provider key, a payment secret, or a database credential belongs in your backend’s environment. The app calls your backend with the user’s session token, and your backend calls the third party.
Decisions live on the server. The app can show or hide a screen using what it knows, but it cannot be the final authority on “is this user signed in?” or “is this user subscribed?”. Treat the app’s view as a cache of the server’s answer. This is the same rule as in why subscription entitlements belong on the server.
How should sign-in work between an Expo app and a backend?
The usual flow has four steps.
- The app asks the user to sign in with Apple, Google, or email.
- The provider returns a signed identity token to the app.
- The app sends that token to your server.
- Your server verifies the token, finds or creates the user, and returns its own session token.
On iOS, the expo-apple-authentication library gives the app an identity token from Sign in with Apple. Apple’s documentation describes how a server verifies the user. For Google, the ID token goes to your server, which validates it as described in Google’s guide to authenticating with a backend server.
Two rules matter more than the library you choose.
Verify, do not decode. Reading the claims inside a token does not prove it is genuine. Check the signature, the issuer, the audience (your app’s identifier), and the expiry. A server that only decodes the token accepts anything a client can type.
Issue your own session. After verification, give the app a session token that your server controls and can revoke. Store it on the device with expo-secure-store, not in plain app storage. The Expo Router documentation shows how to gate screens on that session in its authentication guide. Remember that gating a screen in the app only improves the experience. The server still checks the session on every request.
How do you charge for a subscription in an Expo app?
Store purchases need native code, which means they need a development build and not Expo Go. Expo explains the difference in its introduction to development builds. You then add a purchase library that wraps StoreKit on iOS and Google Play Billing on Android. Apple documents StoreKit, and Google documents Play Billing.
The purchase itself happens in the app. What the purchase means happens on your server.
- The user buys. The store returns a signed transaction (iOS) or a purchase token (Android) to the app.
- The app sends it to your server.
- Your server verifies it with the App Store Server API or the Google Play Developer API, then writes an entitlement for the user.
- The app asks your server for the user’s entitlements and unlocks features from that answer.
After that, the stores tell your server about changes: renewals, expiries, refunds. Apple sends App Store Server Notifications, and Google sends Real-time developer notifications. Handle them idempotently, and treat them as the source of truth for state changes. The table design, grace periods, and refund handling are covered in why subscription entitlements belong on the server. To test each state before real money moves, see testing subscription states before a real purchase.
If the same purchase library and the same backend serve both stores, make sure your entitlement names are your own, such as pro, and not store product identifiers. That keeps the app code the same on both platforms.
How do push notifications work in an Expo app?
Expo’s expo-notifications library handles permission and token registration on the device. Expo’s push notifications overview describes the two ways to send.
- Expo’s push service. The app registers an Expo push token. Your server posts messages to Expo, which forwards them to APNs and FCM. See sending notifications.
- Native tokens. The app registers the native device token. Your server sends directly through APNs and Firebase Cloud Messaging.
Either route needs platform credentials set up first. Expo lists the steps in its push notifications setup guide.
Whichever you choose, the backend work is the same, and it is the part the app cannot do:
- Store tokens as records on a device, and the device on a user. A user has many devices, and tokens change.
- Register the token on every launch and upsert it, so a changed token updates a row.
- Detach the device on sign-out, so the next person on a shared phone does not receive the last user’s messages.
- Remove tokens when the provider reports them invalid, and retry only transient errors.
- Evaluate preferences and quiet hours at send time, with an IANA time zone.
The full data model, with SQL, is in push notification architecture for APNs and FCM. Scheduled reminders are a background job, covered in mobile background jobs: reminders, retries, queues.
What should the server check on every request?
Four checks cover most Expo backends.
- Who is this? Validate the session token. Reject missing or expired ones.
- Does this row belong to them? Enforce ownership in the database, for example with row-level security. See Postgres row-level security for mobile app backends.
- Is their plan allowed to do this? Read the entitlement on the server. Do not trust a flag from the app.
- Is this app version supported? Old builds stay installed for years, so keep API changes additive. See mobile API versioning and migrations for old apps.
If your app also calls an AI model, add a fifth: route the call through your server so the model key stays off the device. Per-plan AI quotas without shipping your API key shows the pattern.
How does this relate to Edict?
Edict is a mobile backend in development. Developers who use React Native and Expo are among the people it is aimed at, but which SDKs ship first has not been decided, and no SDK is available today. The design plans generated TypeScript clients.
The four jobs in this article map to planned modules. The authentication module is designed to cover Apple, Google, and email sign-in with sessions. The subscriptions module is designed to verify store purchases and enforce entitlements on the server. The push notifications module is designed to keep device and token records and send through APNs and FCM. Edict is designed so a coding agent describes these in one configuration file instead of writing the glue by hand. All of this is planned, and the design may change.
For an example app that combines these modules, see fitness and habit tracking. To work on an Expo project with a coding agent, read backend context for an AI coding agent in React Native.
Key takeaways
- An Expo bundle is readable. Keep secrets on a server, and never put them in
EXPO_PUBLIC_variables. - Verify Apple and Google identity tokens on your server, then issue your own revocable session.
- Store purchases need a development build. The server verifies each purchase and keeps entitlements, driven by store notifications.
- Push works with Expo’s push service or native tokens. Either way, store tokens on devices, clean them up, and check preferences at send time.
- Check identity, ownership, plan, and app version on every request.