Environment variables
Every env var the template and its modules read, which ones ship in the app, and where secrets go on EAS.
Every key lives in .env at the repo root (gitignored). bun run setup generates
.env.example with exactly the keys your selection needs, each with a # docs: link to the
dashboard that issues it; copy it to .env and fill in what you use. bun run doctor flags
the required ones that are still empty.
There are two kinds, and the prefix decides which:
EXPO_PUBLIC_*keys are public. Metro inlines them into the JavaScript bundle, so anyone with the app can read them. Only publishable / anon / SDK keys belong here. They're typed insrc/lib/env.tsas optional: a missing key makes its module show a "Configure X" state instead of crashing. Restart withbun run start -- -cafter changing one.- Keys without the prefix are server-only. They're read with
process.env.Xinsidesrc/app/api/**andsrc/server/**(API routes, Pro withbackend=api-routes) and never reach the bundle. Never rename one toEXPO_PUBLIC_. Build-time keys (APP_VARIANT,SENTRY_AUTH_TOKEN, …) work the same way:app.config.tsand config plugins read them while EAS builds, and the app never sees them.
Template
Present in every tree, whatever you picked. The Free tier has no eas.json, so the per-profile
values don't apply there.
| Variable | Kind | Default | What it does |
|---|---|---|---|
EXPO_PUBLIC_APP_VARIANT | public | dev | dev / preview / prod at runtime; set per profile in eas.json |
EXPO_PUBLIC_API_URL | public | - | Base URL for relative requests (data clients, API routes, Stripe, Better Auth). Use https on a device |
APP_VARIANT | build time | dev | Picks the name and bundle-id suffix in app.config.ts; set per profile in eas.json |
EAS_PROJECT_ID | build time | - | Overrides app.easProjectId; feeds extra.eas.projectId and updates.url |
EXPO_PUBLIC_API_URL on a physical iPhone must be https://, a LAN IP, localhost or
*.local: App Transport Security blocks plain http:// to anything else, and src/lib/env.ts
warns in dev when it sees one.
Modules
Only the modules you selected add keys. Every row below is a Pro module (auth, payments,
analytics, crash, backend) except data/apollo, which Starter can pick too; the
backend/api-routes rows, src/server/webhooks included, exist only in a Pro tree set up
with backend=api-routes. Required means the module stays in its
"Configure" state (and doctor fails) until the key is set.
| Variable | Module | Kind | Required | What it is |
|---|---|---|---|---|
EXPO_PUBLIC_SUPABASE_URL | auth/supabase | public | yes | Project URL (Supabase → Project settings → API) |
EXPO_PUBLIC_SUPABASE_ANON_KEY | auth/supabase | public | yes | Anon / publishable key |
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY | auth/clerk | public | yes | Clerk publishable key |
CLERK_SECRET_KEY | auth/clerk | server | for API routes | Clerk secret key (sk_…); lets API routes verify the caller's session token |
CLERK_JWT_KEY | auth/clerk | server | no | JWT public key (PEM), verifies tokens networkless instead of CLERK_SECRET_KEY |
BETTER_AUTH_SECRET | auth/better-auth | server | yes | Session signing secret, openssl rand -base64 32 |
BETTER_AUTH_URL | auth/better-auth | server | yes | Callback base URL, same as EXPO_PUBLIC_API_URL |
DATABASE_URL | auth/better-auth | server | no | Postgres connection string for persistent users and sessions |
APPLE_CLIENT_ID | auth/better-auth | server | no | Apple Services ID for Sign in with Apple |
APPLE_CLIENT_SECRET | auth/better-auth | server | no | Apple client secret (JWT) |
GOOGLE_CLIENT_ID | auth/better-auth | server | no | Google OAuth client id |
GOOGLE_CLIENT_SECRET | auth/better-auth | server | no | Google OAuth client secret |
EXPO_PUBLIC_REVENUECAT_IOS_KEY | payments/revenuecat | public | yes | RevenueCat public SDK key for iOS (appl_…) |
EXPO_PUBLIC_REVENUECAT_ANDROID_KEY | payments/revenuecat | public | yes | RevenueCat public SDK key for Android (goog_…) |
EXPO_PUBLIC_ADAPTY_PUBLIC_KEY | payments/adapty | public | yes | Adapty public SDK key |
EXPO_PUBLIC_ADAPTY_PLACEMENT_ID | payments/adapty | public | no (default) | Placement whose paywall is shown |
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY | payments/stripe | public | yes | Stripe publishable key (pk_…) |
STRIPE_SECRET_KEY | payments/stripe | server | yes | Stripe secret key (sk_…) |
STRIPE_WEBHOOK_SECRET | payments/stripe | server | yes | Webhook signing secret (whsec_…) |
STRIPE_PRICE_ID | payments/stripe | server | yes | Recurring price id (price_…) |
STRIPE_ENTITLEMENT | payments/stripe | server | no (pro) | Entitlement an active or trialing subscription grants |
STRIPE_ALLOW_ANONYMOUS | payments/stripe | server | no (false) | true lets checkout run with auth none, trusting client ids - demos only |
EXPO_PUBLIC_POSTHOG_KEY | analytics/posthog | public | yes | PostHog project API key |
EXPO_PUBLIC_POSTHOG_HOST | analytics/posthog | public | no (US cloud) | https://eu.i.posthog.com for the EU cloud |
EXPO_PUBLIC_AMPLITUDE_KEY | analytics/amplitude | public | yes | Amplitude API key |
EXPO_PUBLIC_SENTRY_DSN | crash/sentry | public | yes | Sentry DSN |
SENTRY_ORG | crash/sentry | build time | no | Org slug, for source map uploads on native builds |
SENTRY_PROJECT | crash/sentry | build time | no | Project slug, same purpose |
SENTRY_AUTH_TOKEN | crash/sentry | build time | no | Auth token with project:releases + org:read; without it, stack traces stay minified |
EXPO_PUBLIC_GRAPHQL_URL | data/apollo | public | no | GraphQL endpoint |
WEBHOOK_SECRET | backend/api-routes | server | no | Shared secret for incoming webhooks under src/server/webhooks |
CLERK_WEBHOOK_SIGNING_SECRET | backend/api-routes | server | no | Signing secret of the Clerk user.deleted webhook (/api/webhooks/clerk) |
POSTHOG_PERSONAL_API_KEY | backend/api-routes | server | no | Lets account deletion erase the user in PostHog |
POSTHOG_PROJECT_ID | backend/api-routes | server | no | PostHog project id, same purpose |
POSTHOG_API_HOST | backend/api-routes | server | no | PostHog API host, same purpose |
AMPLITUDE_API_KEY | backend/api-routes | server | no | Lets account deletion erase the user in Amplitude |
AMPLITUDE_SECRET_KEY | backend/api-routes | server | no | Amplitude secret key, same purpose |
AMPLITUDE_REGION | backend/api-routes | server | no | Amplitude region, same purpose |
REVENUECAT_SECRET_KEY | backend/api-routes | server | no | Lets account deletion erase the customer in RevenueCat |
Each module page under Features has the dashboard steps for its keys. The two
example apps carry their own .env.example with the subset they use.
On EAS
Your .env is gitignored, so EAS never uploads it: a cloud build, eas update and an EAS
Hosting deploy all run without it. A key that works locally but is missing in a TestFlight build
almost always was never set on EAS.
Set each key in the EAS environment it's used in (development, preview, production). A
build picks the environment from its profile: build.<profile>.environment in eas.json if you
add one, otherwise production for store builds (the shipped production profile),
development for dev-client builds (development) and preview for the rest:
bunx eas-cli env:set --environment production --name EXPO_PUBLIC_SUPABASE_URL --value https://xyz.supabase.co --visibility plaintext
bunx eas-cli env:set --environment production --name STRIPE_SECRET_KEY --value sk_live_xxx --visibility sensitive
bunx eas-cli env:list --environment productionenv:set creates or updates a variable (the older env:create still works but is deprecated).
--visibility takes three values:
plaintext- visible on expo.dev, in EAS CLI and in logs. Fine forEXPO_PUBLIC_*keys: they end up in the bundle anyway.sensitive- masked in build and workflow logs, still readable in EAS CLI and on the dashboard behind a toggle. Use it for server keys your API routes read (STRIPE_*,BETTER_AUTH_*,CLERK_SECRET_KEY,WEBHOOK_SECRET): EAS Hosting can't deploysecretvariables, onlyplaintextandsensitiveones. Also use it forSENTRY_AUTH_TOKENif you upload source maps aftereas updatefrom your machine.secret- never readable outside EAS servers, not even by EAS CLI. Right for keys only an EAS build job needs (SENTRY_AUTH_TOKENfor native builds, anNPM_TOKEN). A secret isn't available while EAS CLI resolvesapp.config.tson your machine, can't be pulled, and isn't used byeas update.
bunx eas-cli env:pull --environment development writes that environment's variables to
.env.local (pass --path for another file); secret ones appear only as a commented-out
***** line. Expo loads .env.local on top of .env, so a pulled value wins over the one in
.env. bun run doctor reads only .env, though, so it still lists pulled keys as missing;
pull with --path .env (it asks before overwriting) if you want doctor to see them.
eas update needs --environment on SDK 55 and later - see
Your first update. APP_VARIANT and
EXPO_PUBLIC_APP_VARIANT are already set per profile in eas.json, so don't create them.
Your tree ships no CI workflows. If you add your own (see
Expo's GitHub Actions guide), EAS needs an
EXPO_TOKEN secret (expo.dev → Account settings → Access tokens); set EAS_PROJECT_ID there
too if you'd rather not commit the id.
Pass App Review
Run the store check, fix what it flags, fill in the metadata, and submit to the App Store and Google Play without burning a review cycle.
Update from upstream
Pull fixes and new modules from a newer ReadyNative release into your app - with modules/ kept, after finalizing, and on the Free tier.