12 Nov 2026 · 5 min read

Expo environment variables: which are public?

Expo environment variables end to end: which EXPO_PUBLIC_ values are public, .env file order, EAS env:set visibility, and what replaced eas secret.

An Expo app reads its settings from two places: a .env file on your machine, and environment variables stored on EAS for cloud builds and updates. The name decides where a value ends up. A name with the EXPO_PUBLIC_ prefix is written into the JavaScript bundle, so every user can read it. Any other name stays on your build machine or on your server. This guide answers the questions people search for, starting with the rule that matters most: a secret never goes in an EXPO_PUBLIC_ variable.

Where each kind of variable ends upAn EXPO_PUBLIC_ variable is written into the JavaScript bundle that every user downloads. A variable without the prefix stays on the server, where API routes read it at request time.EXPO_PUBLIC_ prefixno prefixEXPO_PUBLIC_API_URLset in .env or on EASCLERK_WEBHOOK_SIGNING_SECRETset in .env or on EASJavaScript bundleinlined at build time, on every phoneAPI route on your serverread per request, never sent to the appanyone with the app can read itonly your server can read itSecret visibility keeps the value on Expo servers, so it can only feed a build job.

EXPO_PUBLIC_ values are public

The prefix is the whole rule. When Expo bundles your app, it replaces each process.env.EXPO_PUBLIC_NAME in your code with the value from your .env files. That value then ships inside the JavaScript that every device downloads, so anyone with the app can read it. Expo's guide says not to store private keys there. Keep only public values in .env:

# .env (gitignored)
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_SUPABASE_URL=https://xyz.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Read them with a static dot access

Expo inlines a variable only when your code names it as a property of process.env with dot notation. Anything more dynamic is left alone, and the code gets no value:

// Inlined: a static dot access
const apiUrl = process.env.EXPO_PUBLIC_API_URL;

// Not inlined: a computed key, or destructuring
const name = "EXPO_PUBLIC_API_URL";
const missing = process.env[name];
const { EXPO_PUBLIC_API_URL } = process.env;

ReadyNative's client env file follows the same rule. It spells out every key, so the build can inline each one, and it never looks one up by name.

Which .env file wins

Expo picks files by mode. A development run, such as npx expo start, loads these files, and the first one that sets a name wins:

A production run, such as npx expo export, loads the same list with the .production names. Put values that differ per machine, such as your LAN IP, in .env.local, and add .env*.local to .gitignore. On SDK 58 and later, Expo ignores other names such as .env.staging, so stick to these four.

EAS environment variables replace eas secret

Cloud builds and updates never see your local files. Store the values on EAS instead, one variable for each environment. The older commands are deprecated: eas secret:create tells you to use eas env:create, and env:create tells you to use env:set. Use env:set:

bunx eas-cli env:set --environment production --name EXPO_PUBLIC_API_URL --value https://api.example.com --visibility plaintext
bunx eas-cli env:set --environment production --name CLERK_WEBHOOK_SIGNING_SECRET --value whsec_xxx --visibility sensitive
bunx eas-cli env:list --environment production

env:set creates a variable or updates it. Each one takes a visibility, and the table below shows what each setting allows.

VisibilityOn expo.dev and in EAS CLIIn build and workflow logsUsed by eas update and env:pull
plaintextVisibleVisibleYes
sensitiveBehind a toggle on expo.dev; readable in EAS CLIMaskedYes
secretNot readableMaskedNo

Choose the lowest visibility that works. An EXPO_PUBLIC_ value is public whatever its visibility, so plaintext is right for it. EAS Hosting cannot deploy secret variables, so server keys you deploy there need sensitive. Use secret for a value only a build job needs, such as an NPM_TOKEN.

Each environment is its own set of values. A build uses the environment named in its eas.json profile. Without that field, a store build uses production, a development client build uses development, and anything else uses preview.

bunx eas-cli env:pull --environment development   # writes .env.local

env:pull writes the values that its visibility allows to .env.local by default. Expo loads .env.local above .env, so a pulled value wins over the one in .env. Keep that file out of git.

Which values are safe in the client

Ask one question about each value: if someone had it, could they read private data, spend money or act as your account? If yes, it has no prefix and stays on the server. If no, it can carry the prefix.

Safe for the client, so EXPO_PUBLIC_:

Server only, so no prefix:

Server keys: read them in an API route

Server keys have no prefix, so they never reach the bundle. In a Pro tree set up with the api-routes backend, src/server/env.ts reads each one from process.env when a request arrives. The Clerk webhook route uses it like this:

// src/app/api/webhooks/clerk+api.ts
const secret = serverEnv().CLERK_WEBHOOK_SIGNING_SECRET;
if (!secret) return error("Not found", 404);

Without the secret, the route answers 404. On EAS Hosting, set the server keys with sensitive visibility, as shown above.

app.config.ts reads them at build time

app.config.ts runs before the app is built, on your machine or on the build worker. It can read any name with process.env, prefixed or not. The template uses APP_VARIANT this way to pick the app name and bundle id, and sets it in each eas.json profile:

const VARIANT = (process.env.APP_VARIANT ?? "dev") as Variant;

Plaintext and sensitive values are visible to the config when EAS CLI resolves it on your machine. Secret values are not, so any value the config needs must be at least sensitive.

Environment variables and EAS Update

On SDK 55 and later, eas update requires the --environment flag. When you pass it, the project's .env files are ignored, and the update uses that environment's values:

bunx eas-cli update --environment production

Expo inlines the EXPO_PUBLIC_ values of that environment into the bundle it publishes. So a value you change on EAS reaches users only with a new update. Secret values are not available to the update at all.

Common mistakes

Read next: Expo push notifications, which walks through the EAS project id and the push credentials, then the environment variables page for the full key list.

FAQ

What replaced eas secret?

The eas secret commands are deprecated. Use eas env:set, which saves a variable for one or more environments with visibility plaintext, sensitive or secret. Use secret only for values that a build job needs.

Are EXPO_PUBLIC_ variables secret?

No. Expo writes each EXPO_PUBLIC_ value into the JavaScript bundle, so anyone with the app can read it. Put only public values such as URLs and publishable keys there.

Why is EXPO_PUBLIC_API_URL undefined?

Check that the code reads it with a static dot access, that the key is set for the environment your build uses, and that the dev server restarted with the cache cleared (bun run start -- -c).

Is CLERK_WEBHOOK_SIGNING_SECRET safe in the app?

No. It has no EXPO_PUBLIC_ prefix, so it stays out of the bundle. The template's Clerk webhook API route reads it on the server, and EAS Hosting needs it set with sensitive visibility.

More guides