Expo Development Patterns
PLExpo development with Claude Code, Codex, or Cursor works when the agent knows two facts first: the pinned Expo SDK (SDK 57 is current on npm as of 2026-09-26) and whether the app runs in Expo Go or a development build. Expo’s official skills, a fingerprint-gated EAS pipeline, and Maestro flows then make each change verifiable without reading every line.
This page is for developers and tech leads who ship an Expo app to the stores. A teammate adds react-native-vision-camera, Expo Go stops loading the app, and the agent keeps writing code for whichever setup it guessed. A week later an over-the-air (OTA) update reports success, yet no phone picks it up, because the update targets a runtime that no installed build has.
Before you start: this page assumes the React Native basics (navigation, state, native modules) from React Native development patterns, and an agent that can install skills, as described in how to install and manage skills.
What you’ll set up on this page
Section titled “What you’ll set up on this page”- An agent setup with Expo’s official skills and MCP server, installed per tool
- A scaffolding prompt that pins the SDK and the build type, so the agent stops guessing
- A copy-paste prompt for an Expo Router auth gate, and the Maestro flow that proves it works
- A fingerprint-gated EAS Workflows pipeline that decides between a store build and an OTA update for you
- A staged OTA rollout with the commands to watch it and roll it back
- A “what breaks” map for SDK upgrades, Expo Go mismatches, credentials, and runtime mismatches
Which Expo versions does this page assume?
Section titled “Which Expo versions does this page assume?”Tell the agent the versions. Its training data spans several SDKs, and each SDK changes APIs, config keys, and the React Native version under it. The table lists what npm served on 2026-09-26.
| Package | Version on 2026-09-26 | Note |
|---|---|---|
expo | 57.0.25 (SDK 57, released 2026-06-30) | the sdk-56 dist-tag still receives patches; SDK 58 is on the next dist-tag, so check npm view expo dist-tags before pinning |
react-native / react | 0.86.3 / 19.2.3 | the versions SDK 57’s bundledNativeModules.json pins |
expo-router | 57.0.23 | Stack.Protected takes a guard prop |
eas-cli | 24.8.0 | fingerprint, workflow, and update-rollout commands used below |
nativewind | 4.2.7 | 5.0 is a release candidate (5.0.0-rc.0); pin one major |
Never let the agent write a version into package.json by hand. npx expo install <package> picks the version that matches your SDK, and npx expo install --check lists every package that drifted.
Set up Expo skills and the Expo MCP server
Section titled “Set up Expo skills and the Expo MCP server”Expo publishes its own agent skills in expo/skills (about 2.6k stars on GitHub on 2026-09-26). The Expo plugin for Claude Code and Codex (1.13.7 on 2026-09-26) bundles 24 skills, such as expo-router, expo-upgrade, eas-update, and eas-workflows. It also registers Expo’s remote MCP server at https://mcp.expo.dev/mcp. The skills load on demand, so they cost little context until a task needs one. MCP servers cost context per tool; Claude Code and Codex both defer tool definitions behind tool search, so the cost stays lower there, but every server you connect still adds to it.
# Skills + Expo MCP server in one pluginclaude plugin install expo@claude-plugins-officialTo add only the MCP server, run claude mcp add --transport http expo https://mcp.expo.dev/mcp, then open /mcp in a session to complete any sign-in it asks for. Without --transport http, Claude Code 2.1.283 saves the URL as a stdio command, and the connection fails later.
# Skills + Expo MCP server in one plugincodex plugin add expo@openai-curatedTo add only the MCP server, run codex mcp add expo --url https://mcp.expo.dev/mcp, then codex mcp login expo if the server asks for OAuth. Do not run both: the plugin already registers the server.
Install the skills with the cross-agent skills CLI (skills 1.7.0 on npm):
npx skills@latest add expo/skills --skill '*' -a cursorThen add the MCP server to .cursor/mcp.json:
{ "mcpServers": { "expo": { "url": "https://mcp.expo.dev/mcp" } }}For how skills and MCP servers compare, see how to install and manage skills and the frontend skills roundup.
Scaffold an Expo app the agent cannot misread
Section titled “Scaffold an Expo app the agent cannot misread”The prompt matters less than the two facts it pins. Put them in AGENTS.md (or CLAUDE.md) as well, so every later session inherits them.
The fingerprint runtime-version policy matters later. It hashes everything native in the project, so any native change produces a new runtime version by itself. That removes the most common OTA accident: a JavaScript update that calls a native module the installed build does not have.
Run claude --permission-mode manual in an empty directory and paste the prompt. The expo-overview skill routes the task to expo-project-structure and expo-dev-client. Manual mode makes every eas command wait for your approval. From v2.1.283 (both release channels on 2026-10-12), Claude Code starts interactive sessions in auto mode, where a classifier approves commands for you.
Run codex in the empty directory and paste the prompt. To let Codex edit and run local commands but ask before anything outside the workspace, start it with codex --sandbox workspace-write -a on-request.
Paste the prompt into the Agent chat. Review the diff of eas.json and app.json before you accept the change: those two files decide what every later build does.
Build an auth-protected Expo Router layout
Section titled “Build an auth-protected Expo Router layout”Most Expo Router bugs sit in the auth gate. A redirect before the root layout mounts triggers a “navigate before mounting the Root Layout” warning. A redirect during render flashes protected content for a frame. Stack.Protected removes both, because the router never registers the protected screens while the guard is false.
The agent should produce this tree:
app/ _layout.tsx # SessionProvider + Stack.Protected guards (auth)/ _layout.tsx login.tsx (tabs)/ _layout.tsx # Tabs index.tsx profile.tsx.maestro/ auth.yamlYou do not need to read the layout to accept it. Search it for router.replace and <Redirect: a hit inside the render path means the redirect flash is back, so send it back with “move the guard into Stack.Protected”. Then run the Maestro flow on a simulator with the development build installed (maestro test .maestro/auth.yaml). The flow is the acceptance criterion: sign-in, a protected tab, and sign-out either pass on a real runtime or they do not.
Install the Maestro CLI once per machine with curl -fsSL "https://get.maestro.mobile.dev" | bash, the command in the Maestro README (the repository’s main branch sets CLI version 2.11.0 on 2026-09-26). It needs Java 17 or newer, and maestro test needs a booted simulator or emulator with the development build already installed.
Keep styling in one theme with NativeWind
Section titled “Keep styling in one theme with NativeWind”The usual NativeWind failure is a wall of className="bg-[#1d4ed8]" values. A rebrand then touches every screen. Ask for semantic tokens so colours live in one place.
The last sentence turns the style rule into a gate. You stop reviewing colours by eye, because the check fails on the first hard-coded hex value. More UI recipes for React Native and Flutter are in mobile UI recipes.
Ship with a fingerprint-gated EAS pipeline
Section titled “Ship with a fingerprint-gated EAS pipeline”The riskiest decision in an Expo release is “store build or OTA update?”. An agent should not make it by judgement. eas-cli 24.8.0 ships a deploy workflow template that makes the decision mechanically: eas workflow:create saves it as .eas/workflows/deploy.yml by default, modelled on Expo’s deploy-to-production example. A fingerprint job hashes the native project, and a get-build job looks for an existing production build with that hash. If one exists, the workflow publishes an update. If not, it builds and submits to the stores.
You verify the pipeline without reading every YAML key:
workflow:validatepasses for both files.- The production file contains jobs of type
fingerprint,get-build,build,submit, andupdate, and eachupdatejob’sif:checks that itsget-buildjob returned abuild_id. Search fortype:, not for job names: the template names its jobsget_ios_build,build_android, and so on. - Before you merge a native-looking change, compare your working tree with the latest production build. Get the build ID with
eas build:list --platform ios --build-profile production --limit 1 --json, then runeas fingerprint:compare --build-id <BUILD_ID>(add--non-interactivein CI). Repeat for Android: run the sameeas build:listwith--platform androidand compare against that build ID. Bareeas fingerprint:compareopens an interactive picker, so an agent or a CI step cannot run it unattended. A different hash means the next push triggers a store build, not an OTA update.
If your team runs CI on GitHub Actions instead, keep the same decision logic and have the agent read the existing workflows first through the GitHub MCP server. The generic pipeline patterns are in CI/CD pipelines with agents.
Roll out an OTA update in stages
Section titled “Roll out an OTA update in stages”An OTA update reaches every user on a channel within minutes, so ship it the way you would ship a server change: to a slice first.
-
Publish to 10% of users on the production branch:
Terminal window eas update --branch production --message "Fix checkout total rounding" --rollout-percentage 10 -
Watch launches and crashes for the update group:
Terminal window eas update:insights <GROUP_ID> --days 1 -
If the numbers match the previous update, widen the rollout with
eas update:edit <GROUP_ID> --rollout-percentage 100. -
If crashes rise, revert with
eas update:revert-update-rollout, or roll back to an earlier update or the embedded bundle witheas update:rollback.
GROUP_ID is the update group ID that eas update prints. Hand the agent this sequence as a runbook, not as permission. It can prepare the commands and read update:insights, and a person approves each widening step. The general pattern is in progressive delivery.
Wire push notifications end to end
Section titled “Wire push notifications end to end”Agents usually stop after the permission prompt. A working setup also sends the token to your backend, refreshes it, and routes a tapped notification to the right screen.
The Jest test proves the mapping from payload to route. The Maestro flow proves that the route opens on a device. Delivery itself still needs one manual send to a physical device per platform, because simulators do not reproduce every push path.
How you verify agent output without reading every line
Section titled “How you verify agent output without reading every line”Expo gives you enough machine-checkable signals that review becomes a check of evidence, not of code.
| Gate | Command | What it catches |
|---|---|---|
| Types, lint, unit tests | npm run check (tsc --noEmit, expo lint, jest --ci) | wrong API shapes, dead imports, logic regressions |
| Dependency drift | npx expo install --check and npx expo-doctor | versions outside the SDK’s range, duplicate native modules |
| Native change | eas fingerprint:compare --build-id <BUILD_ID> --non-interactive | a JavaScript-only PR that changed the runtime |
| Behaviour | Maestro flows in .maestro/ | auth gates, deep links, and navigation on a real runtime |
| Release health | eas update:insights and Sentry | crashes and launch failures per update group |
Make the agent attach the output of each gate to the pull request. The reviewer then reads what changed in behaviour, the Maestro result, and whether the fingerprint moved. That is the same evidence bundle the rest of the site uses. For crash triage, the @sentry/react-native config plugin uploads source maps during EAS builds, and the Sentry MCP server (https://mcp.sentry.dev/mcp) lets the agent read the symbolicated trace itself. Device testing beyond Maestro, such as device farms and visual checks, is covered in mobile app testing.
What breaks in Expo projects, and how do you recover?
Section titled “What breaks in Expo projects, and how do you recover?”- An SDK upgrade leaves native dependencies behind. Bumping
expois the easy part. Runnpx expo-doctor, paste the full output, and use this prompt: “We upgraded to Expo SDK 57. Use the expo-upgrade skill. Fix each mismatch withnpx expo install --fix, and list every third-party package with no SDK 57 support instead of forcing a version.” Then rebuild the development client, because the old one no longer matches. - It works in Expo Go but not in the development build, or the reverse. The cause is almost always a module that is not in Expo Go, or a config plugin that applies only during
prebuild. Tell the agent which environment fails and ask it to check whether the module needs a config plugin entry inapp.jsonand a new development build. eas buildfails on signing. Paste the build log and ask the agent to name the failing credential, for example an expired distribution certificate or a missing push key. Do not let it regenerate credentials:eas credentialschanges shared signing state for the whole team, so a person runs it.- An update “succeeds” but no device receives it. The update’s runtime version matches no installed build. Take the latest store build’s ID from
eas build:list --platform ios --build-profile production --limit 1 --jsonand runeas fingerprint:compare --build-id <BUILD_ID>. If the hashes differ, the change needs a store build, and the fingerprint-gated workflow would have produced one. - A bad update is already live. Run
eas update:revert-update-rolloutif it is still in a staged rollout, oreas update:rollbackotherwise. Then add a Maestro flow that reproduces the bug, so the next pull request fails before it ships.
Next steps
Section titled “Next steps”- React Native development patterns for navigation, offline state, list performance, and native modules without Expo’s services
- Native iOS and Android recipes when you drop into Swift or Kotlin for a custom module
- Flutter development patterns if you are comparing cross-platform stacks
- API design patterns for the
/api/devicesendpoint and the sync API your app calls
Did this guide help you?
One tap, no sign-in.
What will you do with it?
How did it read?
What got in the way? Pick any that apply
Your role?
Thanks. I read every answer. Michał Jaskólski
Did this guide help?
One tap each. Close whenever you like.
no sign-in