React Native Flow

← Back to the blog

March 10, 2026·10 min read

Expo env vars without leaking secrets

Anything prefixed `EXPO_PUBLIC_` is visible to anyone who installs your app. That’s fine for map tile keys you’d expose anyway; it’s not fine for payment signing secrets. This note is about drawing the line clearly: what belongs in the client bundle, what belongs on a server, and how to keep `.env` files from becoming mystery meat for the next developer.

What actually ships to the phone

Metro inlines public env vars at build time. Treat them like strings committed to git that the whole world can read. If you wouldn’t paste it in a public gist, don’t put it in `EXPO_PUBLIC_`.

For real secrets, you need a backend—or Expo’s server features / EAS patterns—not a hidden string in the client.

.env discipline

Keep `.env` out of git, document required keys in `.env.example`, and fail fast in dev when something’s missing so you’re not debugging `undefined` in production.

Different keys for dev/staging/prod save you from “it worked on my machine” with the wrong base URL.

Third-party SDK keys

Many SDKs want a “public” key (analytics, maps) and a separate server key for privileged operations. Read their docs twice; mobile teams get burned when both look like random strings.

Rotate keys if you ever leaked one in a screenshot or a support ticket.

A simple decision table

Ask three questions for each value: (1) Would a competitor abuse it if they extracted it from the APK/IPA? If yes, it does not belong in `EXPO_PUBLIC_`. (2) Does the vendor call it a “publishable” or “client” key? Match their language, not your guess. (3) Does your privacy policy describe what you collect? Env vars should match that story.

When in doubt, proxy privileged calls through your API and keep tokens server-side. The extra hop is cheaper than an incident review.

Threat model for mobile env vars

Assume anyone can extract strings from your shipped binary. Tools exist to unpack APKs and IPAs, search for keys, and replay API calls. That does not mean every key must be secret—maps and analytics often rely on client-identifiable keys with server-side restrictions. It does mean you classify each value: public-by-design, sensitive but client-embedded with mitigations, or must-never-ship-on-device. The classification should match legal and security reviews, not only engineering convenience. Document who can rotate a key, how rotation propagates to apps in the field, and whether older app versions must keep working during rotation windows. When you add a new `EXPO_PUBLIC_` variable, ask what an attacker gains if they read it tomorrow. If the answer is “nothing meaningful because the backend enforces auth and rate limits,” you are probably fine. If the answer is “they can bill our cloud provider or read private data,” move the secret to the server. Periodically audit your env usage the same way you audit dependencies—keys accumulate like lint.

Build-time inlining and surprises in CI

Metro replaces `process.env.EXPO_PUBLIC_*` at bundle time. That implies different values can land in different build flavors—staging, production, preview—without runtime checks if you configure CI correctly. It also implies local mistakes: forgetting to restart Metro after editing `.env`, or building a release with the wrong env file symlinked. Make CI print non-secret indicators of which environment config was picked—maybe a short hash of the env file name or a non-sensitive label—into build logs for traceability. For EAS Build, profile-specific `env` blocks are your friend; duplicate less by inheriting defaults and overriding only what differs. Watch for accidental commits of `.env` files with real secrets; pre-commit hooks or secret scanners pay for themselves quickly. Remember web builds and native builds might load env differently if you have a shared package—centralize validation so both paths fail loudly when required keys are missing.

Backend pairing and least privilege

Client keys should align with backend policies: IP allowlists where possible, referrer checks for web-ish keys, separate keys per platform if vendors support it, and monitoring for anomalous usage. If your mobile app talks directly to a third-party API, ensure that API’s dashboard alerts you to quota spikes. Prefer server-mediated access for anything that bills per call or exposes user-specific data. When you must call third parties from the client, scope tokens narrowly and rotate them on suspicious activity. Document the data path in your privacy policy and internal architecture docs—onboarding engineers should not have to guess which env vars touch PII. For OAuth, PKCE and short-lived tokens are table stakes; never ship long-lived refresh tokens in public env vars—that is not what `EXPO_PUBLIC_` is for, but teams have tried. Pair mobile changes with backend changes in the same release train when possible to avoid half-deployed states.

Developer experience versus safety

Strict env validation can annoy developers if every local experiment requires ten variables. Mitigate with sensible defaults for local dev, clear `.env.example`, and fast failure messages that say exactly which key is missing—not a generic undefined error deep in a fetch wrapper. Consider a small debug overlay in internal builds showing environment name and build time—never secrets. For feature branches, document which backend URL to use and how to obtain test credentials without pasting them into Slack. When someone adds a new required key, update onboarding docs the same day. The worst failure mode is “prod works, new hire cannot run the app” because undocumented env assumptions pile up. Balance is achievable: safety in production, frictionless defaults in dev, and loud errors at the boundary between them.

Rotations, incidents, and communication

When a key leaks—screenshot, ticket attachment, public repo—assume compromise. Rotate the key, audit usage logs if available, and ship a client update if the key cannot be revoked server-side instantly. Communicate timelines to support and marketing so user-facing docs stay accurate. Afterward, add a postmortem bullet: could tooling have prevented the leak? Often the answer is secret scanning in CI or fewer people needing the full key at all. For non-leak incidents, like a vendor deprecating an endpoint, track sunsetting alongside your app versions in the field. Long upgrade tails mean you may need backward-compatible servers longer than you want. Env vars are one piece of that compatibility story—version headers and feature flags are others.

Long-term hygiene checklist

Quarterly, review all `EXPO_PUBLIC_` entries and remove unused ones. Confirm `.env.example` matches required keys. Verify staging and prod differ only where intended. Ensure new engineers can bootstrap in under thirty minutes following docs. Confirm crash and analytics dashboards segment by build flavor so you do not debug prod issues against staging data. When you adopt new Expo SDKs, re-read env handling notes—tooling evolves. Finally, tie env practices to your compliance story: SOC2 and similar audits love clear evidence that secrets are not in repos and that access is controlled. Good env hygiene is boring; boring is what lets you sleep during releases.

EAS profiles, channels, and env per binary

Expo Application Services lets you define build profiles that pair environment files with credentials and native settings. Treat each profile as a release vehicle: preview builds for QA, staging builds for internal dogfood, production for stores. Align env files so that switching profiles never accidentally points preview apps at production backends—or worse, the inverse. Document which profile marketing uses for demos; demos leaking into analytics dashboards pollutes metrics. When you promote a build from staging to production, verify env parity for non-secret config like API base URLs and feature flag endpoints. Automate checks that fail builds when required public keys are missing rather than shipping blank maps or silent analytics. Channels for OTA updates interact with env: a JS bundle compiled against the wrong `EXPO_PUBLIC_` set can mismatch native code. Version your runtime expectations and refuse incompatible bundles. The theme is the same everywhere: make wrong configurations loud, not subtle.

Shipping and reliability habits (1)

Metro cache issues masquerade as logic bugs. Establish a documented reset ladder: dev server restart, cache flags, Watchman, derived data, then dependency reinstalls. Compare platforms when only one breaks—native steps diverge. Keep CI caches deterministic with lockfiles and pinned toolchains.

Environment variables should be classified: public-by-design, sensitive-with-mitigations, or never-on-device. `EXPO_PUBLIC_` values are extractable—treat them that way. Align env handling across EAS profiles and local dev; fail fast when keys are missing instead of shipping undefined behavior.

Platform differences worth rehearsing (2)

Security and privacy expectations move faster than roadmaps. Treat analytics, crash, and attribution SDKs as part of your threat model: initialize them deliberately, document data flows, and verify ‘off’ truly stops network calls. Client-side secrets are public secrets—anything shipped in an APK or IPA should be assumed extractable. Pair mobile changes with backend policies so authorization remains consistent across platforms.

Accessibility is compatibility. Labels, focus order, and dynamic type are not polish—they determine whether users can complete tasks at all. Test with VoiceOver and TalkBack on hardware; simulators miss focus bugs. When designs prioritize minimalism, negotiate text alternatives for icon-only controls. Accessibility regressions often follow navigation redesigns—add checklist items to those PRs specifically.

Security, privacy, and data handling (3)

Push notifications walk a line between helpful and intrusive. Prime users with context, respect notification channels on Android, and measure opt-outs after campaigns—spikes mean copy or frequency problems. Payload design affects background behavior; test killed and locked-device states. Tokens belong server-side with rotation strategies; never treat the client as authoritative for subscription state.

Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.

Performance and measurement discipline (4)

OTA updates are powerful and risky: runtime compatibility, rollback plans, and user-visible behavior changes need governance. Channels should map to release maturity—staging versus production—with access controls on publish credentials. Large assets over cellular need care; silent failures erode trust more than a frank ‘update failed, retry’ message.

Keyboard and form UX separate polished apps from ‘works on desktop simulators.’ Platform differences in soft input modes matter; test smallest phones and Android gesture navigation. Primary actions must remain reachable when the keyboard is visible—scroll containers and keyboard controllers exist because this problem is universal.

Team process and long-term maintenance (5)

In-app purchases require server validation, restore flows, and support tooling that respects privacy. Sandbox quirks are normal—budget QA time. Subscriptions interact with family sharing, regional pricing, and refunds; engineering must stay aligned with finance and legal narratives users see in receipts.

Privacy nutrition labels and Play Data Safety forms should reflect actual SDK behavior—inventory dependencies each release and remove dead code. Drift between claims and telemetry is legal and store risk, not just embarrassment. Involve legal early when adding analytics or ads.

Shipping and reliability habits (6)

Testing onboarding changes with funnel metrics beats debating opinions. Segment by acquisition channel and platform; back behavior differs. Skip paths must be genuine—dark patterns may win short metrics and destroy brand trust. Localization length tests prevent clipped CTAs in verbose languages.

Background execution policies change with OS updates—revalidate after major iOS and Android releases. Misused background modes invite rejection. Persist user work frequently; the OS can kill you anytime after backgrounding. Uploads and timers should tolerate pause and resume without corrupting state.

Platform differences worth rehearsing (7)

Project structure should make ownership obvious: routes as backbone, feature folders for product areas, thin screens, and shared infrastructure that is deliberately named. Refactor in vertical slices with device-tested releases—big-bang rewrites without tests are how teams lose weeks.

Helper modules concentrate glue code—storage, navigation, permissions—so screens stay readable. Split helpers by topic before files become merge-conflict magnets, and document each module’s contract. Good helpers answer ‘where do we save tokens?’ in one glance—not ‘ask Sarah.’

Security, privacy, and data handling (8)

Internationalization is a product feature, not a string swap. Plural rules, RTL layout, and locale-aware formatting change behavior—not just copy length. Pseudolocale helps find clipping early, but real Arabic and German QA catches nuance. Avoid concatenating translated fragments; context matters. Document glossary terms so translators do not invent inconsistent product names.

Monorepos amplify both leverage and failure modes: duplicate React versions cause mysterious hook errors, and Metro misconfiguration blocks local packages from resolving. Invest in workspace discipline—single React version, documented `watchFolders`, and lint rules preventing packages from importing app navigators accidentally. CI must mirror local installs; ‘works on my laptop’ with different package managers is a time bomb.

Sponsored

Quick promo