Expo / React Native
Expo and React Native projects use the Metro bundler, which has its own approach to environment variables. Varlock integrates via a Babel plugin that replaces ENV.xxx references with their resolved values at compile time, and a Metro config wrapper that initializes the ENV proxy at runtime for server routes.
To integrate varlock into an Expo / React Native application, you must use our @varlock/expo-integration package.
This integration does a few things:
- Loads and validates your
.envfiles using varlock at bundle time - Replaces
ENV.xxxreferences to non-sensitive config items with their literal values at compile time - Sensitive values are never inlined into your bundle — they are accessible at runtime only in Expo Router API routes (
+apifiles) - Patches the global console to redact sensitive values from logs
-
Install varlock and the Expo integration package
Terminal window npm install @varlock/expo-integration varlockTerminal window pnpm add @varlock/expo-integration varlockTerminal window bun add @varlock/expo-integration varlockTerminal window yarn add @varlock/expo-integration varlockTerminal window vlt install @varlock/expo-integration varlock -
Run
varlock initto set up your.env.schemafileThis will guide you through setting up your
.env.schemafile, based on your existing.envfile(s). Make sure to review it carefully.Terminal window npm exec -- varlock initTerminal window pnpm exec -- varlock initTerminal window bun exec varlock initTerminal window vlx -- varlock initTerminal window yarn exec -- varlock init -
Add the Babel plugin to your
babel.config.jsbabel.config.js module.exports = {presets: ['babel-preset-expo'],plugins: [require('@varlock/expo-integration/babel-plugin'),],}; -
Wrap your Metro config
Wrap your Metro config with
withVarlockMetroConfig. This automatically configures Metro to resolve varlock’s subpath exports (e.g.varlock/env) and initializes theENVproxy in the main Metro process so that sensitive values are available at runtime in server routes.metro.config.js const { getDefaultConfig } = require('expo/metro-config');const { withVarlockMetroConfig } = require('@varlock/expo-integration/metro-config');const config = getDefaultConfig(__dirname);module.exports = config;module.exports = withVarlockMetroConfig(config);
Accessing environment variables
Section titled “Accessing environment variables”Rather than using process.env.SOMEVAR directly, use varlock’s ENV object for better type-safety:
import { ENV } from 'varlock/env';
// Non-sensitive values are inlined at bundle time:const apiUrl = ENV.API_URL; // ✨ recommended — replaced at compile time
// process.env still works, but loses type-safety and compile-time replacement:const legacyUrl = process.env.API_URL; // 🆗 still worksWhy use ENV instead of process.env?
Section titled “Why use ENV instead of process.env?”- Non-string values (e.g., number, boolean) are properly typed and coerced
- Non-sensitive items are replaced with their literal values at bundle time (smaller bundle, no runtime lookup)
- Sensitive items are never embedded in the bundle
- Better error messages for invalid or unavailable keys
How compile-time replacement works
Section titled “How compile-time replacement works”When Metro compiles your project, the Babel plugin traverses the AST and replaces ENV.xxx member expressions:
import { ENV } from 'varlock/env';const url = ENV.API_URL;const port = ENV.PORT;const debug = ENV.DEBUG;const url = "https://api.example.com";const port = 3000;const debug = false;Sensitive items (marked with @sensitive in your .env.schema) are never replaced — they remain as ENV.xxx references and are resolved at runtime via the proxy in server routes.
Type-safety and IntelliSense
Section titled “Type-safety and IntelliSense”To enable type-safety and IntelliSense for your env vars, enable the @generateTypes root decorator in your .env.schema. Note that if your schema was created using varlock init, it will include this by default.
# @generateTypes(lang='ts', path='env.d.ts')# ---# your config items...Managing multiple environments
Section titled “Managing multiple environments”Varlock can load multiple environment-specific .env files (e.g., .env.development, .env.preview, .env.production) by using the @currentEnv root decorator.
Usually this env var will be defaulted to something like development in your .env.schema file, and you can override it when running commands. For Expo projects, you can set it in your package.json scripts or via EAS environment variables:
{ "scripts": { "start": "expo start", "build:staging": "APP_ENV=staging eas build", "build:production": "APP_ENV=production eas build", }}See the environments guide for more information.
Managing sensitive config values
Section titled “Managing sensitive config values”Sensitive values (marked with @sensitive) are never statically inlined into your bundle — regardless of any prefix conventions. You control this with the @defaultSensitive root decorator and the @sensitive item decorator. See the secrets guide for more information.
Set a default and explicitly mark items:
# @defaultSensitive=false# ---NON_SECRET_FOO= # non-sensitive - will be inlined into bundle# @sensitiveSECRET_KEY= # sensitive - will NEVER be inlined, runtime onlySensitive values in server routes
Section titled “Sensitive values in server routes”If you use Expo Router API routes (+api files), sensitive values are accessible at runtime via the ENV proxy. These files run server-side in the Metro process where withVarlockMetroConfig has initialized the environment.
import { ENV } from 'varlock/env';
export function GET() { // ✅ Sensitive values work in +api server routes const key = ENV.SECRET_KEY; return Response.json({ authorized: !!key });}Sensitive values in native code
Section titled “Sensitive values in native code”In native app code (anything that isn’t a +api server route), sensitive values are not available. React Native apps run entirely on the device — there is no server to keep secrets safe. Accessing a sensitive value in native code will throw at runtime.
The Babel plugin also emits a build-time warning when it detects a sensitive ENV.xxx reference in a non-server file, helping you catch these issues early.
import { ENV } from 'varlock/env';
const url = ENV.API_URL; // ✅ non-sensitive — inlined at build timeconst key = ENV.SECRET_KEY; // ❌ throws at runtime, build-time warning