Stack Overview
Expo is best understood as three layers:
flowchart TD
product["Product code"] --> expo["Expo app framework"]
expo --> nativeBinary["Android and iOS binaries"]
expo --> eas["Optional EAS services"]
product --> js["JavaScript bundle and assets"]
product --> appConfig["App config"]
appConfig --> nativeBinary
js --> nativeBinary
eas --> nativeBinary
What Runs Where
| Piece | Lives in | Release implication |
|---|---|---|
| React components, hooks, stores | JavaScript bundle | Can usually ship by app build or compatible OTA update |
| Images, fonts, copy | Assets and bundle | Can usually ship by app build or compatible OTA update |
| Expo Router route tree | JavaScript bundle plus deep-link config | Route code can be OTA if runtime-compatible; native link config may need rebuild |
| App identifiers, icons, splash, permissions | Native binary generated from app config | Requires rebuild and store release |
| Native libraries and modules | Native binary | Requires development build and release build |
| EAS Build/Submit/Update config | EAS project plus eas.json and app config | Affects build, store upload, and OTA targeting |
The Practical Mental Model
An Expo app is not just JavaScript. It is a native Android/iOS app containing:
- Native runtime and dependencies.
- JavaScript bundle.
- Assets.
- App identifiers, permissions, icons, splash, and platform config.
- Optional update client that can fetch compatible JavaScript/assets later.
That model prevents the two common mistakes: treating native changes as OTA-safe and treating Expo Go as proof of release behavior.