ReadyNative
BackendPro

Deploy your API routes

Put Expo Router API routes on EAS Hosting, give them server keys, smoke-test /api/health, and point every build profile at the deployed URL.

Pro

Hey - by the end of this page your API routes answer at https://your-app.expo.app, with their server keys set on EAS, and your builds call that URL instead of your laptop.

While you develop, bun run start serves src/app/api/** from the dev server. A release build has no dev server, so anything that talks to your routes - Stripe Checkout, Better Auth, account deletion - needs them deployed first.

Before you start

  • Tier: Pro. The backend=api-routes module isn't in Free or Starter.
  • Time: about 30 minutes.
  • Runs in: anything - the routes run on EAS Hosting, not on the device, so Expo Go works too.
  • Accounts: an Expo account with a linked EAS project (Ship to TestFlight has the commands). EAS Hosting has a free tier.
  • Previous tutorial: Add analytics and crash reporting.

Picked backend=api-routes at setup? Skip step 1. Step 1 needs modules/, so it works only in a tree set up with --keep-modules; a finalized tree without it starts from a fresh clone - see Can I change a module after setup?.

1. Add the module

bun run setup --backend api-routes --yes --keep-modules

It patches the app config with web.output: "server", which is what makes API routes build (the static web export is off while it's selected), and adds src/app/api/health+api.ts, src/app/api/echo+api.ts, src/app/api/privacy/delete+api.ts and the helpers in src/server/.

You should see: src/app/api/ and src/server/ in your tree.

2. Run the routes locally

bun run start

In another terminal:

curl -s http://localhost:8081/api/health

You should see: {"ok":true,"version":"1.0.0","variant":"dev"} - the version is the one in package.json.

3. Put the server keys on EAS

Deployed routes read their keys from an EAS environment, never from your .env. Server keys (no EXPO_PUBLIC_ prefix) go in as sensitive - not secret, which EAS Hosting can't deploy. EXPO_PUBLIC_* keys your routes read (the Supabase ones, for example) go in as plaintext. Environment variables โ†’ On EAS explains the three visibilities.

bunx eas-cli env:set --name WEBHOOK_SECRET --value your-random-string --environment production --visibility sensitive

The others depend on your stack:

Your stackServer keys
payments=stripeSTRIPE_SECRET_KEY, STRIPE_PRICE_ID, STRIPE_WEBHOOK_SECRET (live values)
auth=better-authBETTER_AUTH_SECRET, BETTER_AUTH_URL (the deployed origin), DATABASE_URL
auth=clerkCLERK_SECRET_KEY (or CLERK_JWT_KEY), CLERK_WEBHOOK_SIGNING_SECRET
auth=supabasenone - EXPO_PUBLIC_SUPABASE_URL and _ANON_KEY as plaintext
Account deletion (optional)POSTHOG_PERSONAL_API_KEY + POSTHOG_PROJECT_ID, AMPLITUDE_API_KEY + AMPLITUDE_SECRET_KEY, REVENUECAT_SECRET_KEY
bunx eas-cli env:list --environment production

You should see: every key your routes read, in the production environment.

4. Deploy

A server deploy runs in this order: pull the environment so the export sees the same values, export the web bundle (which carries the API routes), then deploy with the same environment:

bunx eas-cli env:pull --environment production
bunx expo export --platform web
bunx eas-cli deploy --environment production

env:pull writes .env.local, which also wins over .env in local development - delete it once the deploy is done. The first eas deploy asks you to pick a preview subdomain, say trailmix. Re-run the export before every deploy.

You should see: a preview URL such as https://trailmix--abc123xyz.expo.app and a link to the deployment on expo.dev.

5. Smoke-test it

curl -s https://trailmix--abc123xyz.expo.app/api/health

You should see: {"ok":true,...}. The variant field reads dev on a deploy - it reflects EXPO_PUBLIC_APP_VARIANT, which only builds set.

Happy with it? Promote it to the production URL:

bunx eas-cli deploy --prod --environment production

That deploys dist/ again as the production deployment, at https://trailmix.expo.app.

6. Point the app at it

EXPO_PUBLIC_API_URL is compiled into each build and update, so set it per EAS environment: the production URL for production and, if you want a staging backend, an alias for preview (bunx eas-cli deploy --alias staging gives you https://trailmix--staging.expo.app):

bunx eas-cli env:set --name EXPO_PUBLIC_API_URL --value https://trailmix.expo.app --environment production --visibility plaintext
bunx eas-cli env:set --name EXPO_PUBLIC_API_URL --value https://trailmix--staging.expo.app --environment preview --visibility plaintext

Then ship it: a new build (Ship to TestFlight) or, since it's only a JavaScript value, an update (Your first update):

bunx eas-cli update --channel production --environment production --message "Use hosted API"

Keep http://<lan-ip>:8081 in your local .env for development.

You should see: the installed build reach the hosted routes with your laptop's dev server stopped - for example, the Stripe paywall shows its price, or Better Auth signs you in.

7. Re-wire what depends on it

Anything registered against your dev URL needs the deployed one now:

  • Stripe (/api/stripe/checkout, /entitlements, /webhook, /return): in live mode, add a webhook endpoint at https://trailmix.expo.app/api/stripe/webhook and set its whsec_โ€ฆ as STRIPE_WEBHOOK_SECRET - see Add a paywall.
  • Better Auth (/api/auth/*): BETTER_AUTH_URL must be the deployed origin, and every OAuth redirect URI (https://trailmix.expo.app/api/auth/callback/google, โ€ฆ/apple) re-registered against it. Swap the memory database for Postgres before real users arrive.
  • Account deletion (/api/privacy/delete): Settings โ†’ Delete account calls it before the auth provider deletes the user, to erase them in PostHog, Amplitude and RevenueCat when those server keys are set. It answers 503 until server auth is configured.
  • Clerk (/api/webhooks/clerk): point the user.deleted webhook at the deployed URL.

After changing keys, run step 4 again - a deploy reads the environment at deploy time.

You should see: if you use Stripe, successful deliveries to the new endpoint on the Stripe dashboard's webhook page.

Check it

curl -s https://trailmix.expo.app/api/health
bun run doctor

The health route should answer {"ok":true,...}, and every backend row should be green.

If it doesn't work

  • 404 on every /api/* URL - the export ran without the module's web.output: "server", or you deployed an old dist/. Run bunx expo export --platform web again, then deploy.
  • 500 from a route that works locally - a server key is missing from the environment you deployed with, or it's stored as secret. Fix it with env:set --visibility sensitive and deploy again.
  • The app still calls your laptop - the build or update was made before EXPO_PUBLIC_API_URL was set on EAS, or a leftover .env.local overrode it locally. Publish an update with --environment production.
  • iOS refuses the request - the URL must be https; EAS Hosting always is, so check for a leftover http:// value.
  • Local dev suddenly uses production values - delete the .env.local that env:pull wrote.

More in Troubleshooting.

Congrats ๐ŸŽ‰

Your backend is live on EAS Hosting, with its keys on EAS and every build pointed at it - so Checkout, sign-in and account deletion work without your laptop. To take the module out again, see API routes โ†’ Remove it. For when to outgrow API routes, read Do I need a backend?.

Next, get a build to testers: Ship to TestFlight.

On this page

Get ReadyNative