Expo Workflow Expert
Expert in the Expo managed workflow including EAS Build/Submit/Update, config plugins, custom dev clients, and Expo Router.
Activation Triggers
Activate on: "Expo project", "EAS Build", "EAS Submit", "EAS Update", "config plugin", "expo-dev-client", "Expo Router", "Expo SDK 52", "app.json config", "expo prebuild"
NOT for: Bare React Native → react-native-architect | Native module authoring → react-native-architect | Flutter → flutter-bloc-state-manager
Quick Start
- Create project —
npx create-expo-app@latest my-app
- Configure app.json — set bundle IDs, versioning, splash screen, icons
- Set up EAS —
npx eas init and configure eas.json for build profiles
- Add Expo Router — file-based routing with typed routes
- Create dev client —
npx eas build --profile development for custom native builds
Core Capabilities
| Domain |
Technologies |
| Build |
EAS Build, local builds (npx expo run:ios/android), prebuild |
| Updates |
EAS Update (OTA), update groups, channels, rollbacks |
| Submit |
EAS Submit to App Store Connect and Google Play Console |
| Routing |
Expo Router 4, file-based routing, API routes, typed routes |
| Config |
Config plugins, app.json/app.config.ts, Expo Modules API |
Architecture Patterns
EAS Build Profile Configuration
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {
"ios": {
"appleId": "team@example.com",
"ascAppId": "123456789",
"appleTeamId": "ABCDE12345"
},
"android": {
"serviceAccountKeyPath": "./google-services.json",
"track": "internal"
}
}
}
}
EAS Update Strategy (OTA Updates)
Production Channel:
├─ Branch: production
├─ Update: JS-only changes ship instantly (no binary rebuild)
└─ Rollback: `eas update:rollback --channel production`
Preview Channel:
├─ Branch: staging
├─ Internal testers get latest changes
└─ QA before promoting to production
Flow:
Code change → Is it JS-only?
├─ YES → eas update --channel production --message "fix: ..."
│ (Users get update in < 1 minute, no store review)
└─ NO (native change) → eas build + eas submit
(Full store review cycle)
Config Plugin for Native Customization
// plugins/withCustomSplash.ts
import { ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
const withCustomSplash: ConfigPlugin = (config) => {
return withAndroidManifest(config, async (modConfig) => {
const mainApplication = modConfig.modResults.manifest.application?.[0];
if (mainApplication) {
// Add custom meta-data to AndroidManifest.xml
mainApplication['meta-data'] = mainApplication['meta-data'] || [];
mainApplication['meta-data'].push({
$: {
'android:name': 'expo.modules.splashscreen.SplashScreenImageResizeMode',
'android:value': 'contain',
},
});
}
return modConfig;
});
};
export default withCustomSplash;
// app.config.ts — use the plugin
export default {
plugins: [
'./plugins/withCustomSplash',
['expo-camera', { cameraPermission: 'Allow camera for scanning' }],
],
};
Anti-Patterns
- Ejecting unnecessarily — reaching for
expo eject when a config plugin would suffice. Since Expo SDK 50+, the "managed vs bare" distinction is gone; use npx expo prebuild with config plugins.
- OTA updates for native changes — shipping native module changes via EAS Update. Only JS bundle changes work with OTA; native changes require a new binary build.
- Not using development builds — testing in Expo Go which lacks custom native modules. Create a development build (
expo-dev-client) for full-fidelity testing.
- Hardcoded app.json — static config when dynamic values are needed. Use
app.config.ts for environment-specific bundle IDs, API URLs, and feature flags.
- Skipping update channels — publishing all updates to one channel. Use separate channels (development, preview, production) for staged rollout.
Quality Checklist
[ ] app.config.ts used (not static app.json) for dynamic configuration
[ ] EAS Build configured with development, preview, and production profiles
[ ] EAS Update channels match build profiles
[ ] Development build created (not relying on Expo Go)
[ ] Config plugins used for native customization (no manual native edits)
[ ] Expo Router with typed routes enabled
[ ] Auto-increment build numbers in production profile
[ ] EAS Submit configured for both App Store and Google Play
[ ] OTA updates tested with preview channel before production
[ ] Rollback procedure documented and tested
[ ] Bundle size monitored (expo-optimize for image compression)
[ ] Expo SDK on latest stable version
1---2name: expo-workflow-expert3description: Expo managed workflow expert for EAS Build, Submit, Update, config plugins, and custom dev clients. Activate on: Expo project, EAS Build, EAS Update, config plugin, expo-dev-client, Expo Router, Expo SDK 52, app.json configuration. NOT for: bare React Native (use react-native-architect), native module authoring (use react-native-architect), Flutter (use flutter-bloc-state-manager).4license: Apache-2.05---6
7# Expo Workflow Expert
8
9Expert in the Expo managed workflow including EAS Build/Submit/Update, config plugins, custom dev clients, and Expo Router.
10
11## Activation Triggers
12
13**Activate on:** "Expo project", "EAS Build", "EAS Submit", "EAS Update", "config plugin", "expo-dev-client", "Expo Router", "Expo SDK 52", "app.json config", "expo prebuild"
14
15**NOT for:** Bare React Native → `react-native-architect` | Native module authoring → `react-native-architect` | Flutter → `flutter-bloc-state-manager`
16
17## Quick Start
18
191. **Create project** — `npx create-expo-app@latest my-app`
202. **Configure app.json** — set bundle IDs, versioning, splash screen, icons
213. **Set up EAS** — `npx eas init` and configure `eas.json` for build profiles
224. **Add Expo Router** — file-based routing with typed routes
235. **Create dev client** — `npx eas build --profile development` for custom native builds
24
25## Core Capabilities
26
27| Domain | Technologies |
28|--------|-------------|
29| **Build** | EAS Build, local builds (`npx expo run:ios/android`), prebuild |
30| **Updates** | EAS Update (OTA), update groups, channels, rollbacks |
31| **Submit** | EAS Submit to App Store Connect and Google Play Console |
32| **Routing** | Expo Router 4, file-based routing, API routes, typed routes |
33| **Config** | Config plugins, app.json/app.config.ts, Expo Modules API |
34
35## Architecture Patterns
36
37### EAS Build Profile Configuration
38
39```json
40{
41 "build": {
42 "development": {
43 "developmentClient": true,
44 "distribution": "internal",
45 "ios": {
46 "simulator": true
47 }
48 },
49 "preview": {
50 "distribution": "internal",
51 "channel": "preview"
52 },
53 "production": {
54 "channel": "production",
55 "autoIncrement": true
56 }
57 },
58 "submit": {
59 "production": {
60 "ios": {
61 "appleId": "team@example.com",
62 "ascAppId": "123456789",
63 "appleTeamId": "ABCDE12345"
64 },
65 "android": {
66 "serviceAccountKeyPath": "./google-services.json",
67 "track": "internal"
68 }
69 }
70 }
71}
72```
73
74### EAS Update Strategy (OTA Updates)
75
76```
77Production Channel:
78 ├─ Branch: production
79 ├─ Update: JS-only changes ship instantly (no binary rebuild)
80 └─ Rollback: `eas update:rollback --channel production`
81
82Preview Channel:
83 ├─ Branch: staging
84 ├─ Internal testers get latest changes
85 └─ QA before promoting to production
86
87Flow:
88 Code change → Is it JS-only?
89 ├─ YES → eas update --channel production --message "fix: ..."
90 │ (Users get update in < 1 minute, no store review)
91 └─ NO (native change) → eas build + eas submit
92 (Full store review cycle)
93```
94
95### Config Plugin for Native Customization
96
97```typescript
98// plugins/withCustomSplash.ts
99import { ConfigPlugin, withAndroidManifest } from 'expo/config-plugins';
100
101const withCustomSplash: ConfigPlugin = (config) => {
102 return withAndroidManifest(config, async (modConfig) => {
103 const mainApplication = modConfig.modResults.manifest.application?.[0];
104 if (mainApplication) {
105 // Add custom meta-data to AndroidManifest.xml
106 mainApplication['meta-data'] = mainApplication['meta-data'] || [];
107 mainApplication['meta-data'].push({
108 $: {
109 'android:name': 'expo.modules.splashscreen.SplashScreenImageResizeMode',
110 'android:value': 'contain',
111 },
112 });
113 }
114 return modConfig;
115 });
116};
117
118export default withCustomSplash;
119
120// app.config.ts — use the plugin
121export default {
122 plugins: [
123 './plugins/withCustomSplash',
124 ['expo-camera', { cameraPermission: 'Allow camera for scanning' }],
125 ],
126};
127```
128
129## Anti-Patterns
130
1311. **Ejecting unnecessarily** — reaching for `expo eject` when a config plugin would suffice. Since Expo SDK 50+, the "managed vs bare" distinction is gone; use `npx expo prebuild` with config plugins.
1322. **OTA updates for native changes** — shipping native module changes via EAS Update. Only JS bundle changes work with OTA; native changes require a new binary build.
1333. **Not using development builds** — testing in Expo Go which lacks custom native modules. Create a development build (`expo-dev-client`) for full-fidelity testing.
1344. **Hardcoded app.json** — static config when dynamic values are needed. Use `app.config.ts` for environment-specific bundle IDs, API URLs, and feature flags.
1355. **Skipping update channels** — publishing all updates to one channel. Use separate channels (development, preview, production) for staged rollout.
136
137## Quality Checklist
138
139```
140[ ] app.config.ts used (not static app.json) for dynamic configuration
141[ ] EAS Build configured with development, preview, and production profiles
142[ ] EAS Update channels match build profiles
143[ ] Development build created (not relying on Expo Go)
144[ ] Config plugins used for native customization (no manual native edits)
145[ ] Expo Router with typed routes enabled
146[ ] Auto-increment build numbers in production profile
147[ ] EAS Submit configured for both App Store and Google Play
148[ ] OTA updates tested with preview channel before production
149[ ] Rollback procedure documented and tested
150[ ] Bundle size monitored (expo-optimize for image compression)
151[ ] Expo SDK on latest stable version
152```