Skip to content

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 .env files using varlock at bundle time
  • Replaces ENV.xxx references 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 (+api files)
  • Patches the global console to redact sensitive values from logs
  1. Install varlock and the Expo integration package

    Terminal window
    npm install @varlock/expo-integration varlock
  2. Run varlock init to set up your .env.schema file

    This will guide you through setting up your .env.schema file, based on your existing .env file(s). Make sure to review it carefully.

    Terminal window
    npm exec -- varlock init
  3. Add the Babel plugin to your babel.config.js

    babel.config.js
    module.exports = {
    presets: ['babel-preset-expo'],
    plugins: [
    require('@varlock/expo-integration/babel-plugin'),
    ],
    };
  4. 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 the ENV proxy 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);

Rather than using process.env.SOMEVAR directly, use varlock’s ENV object for better type-safety:

example.ts
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 works
  • 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

When Metro compiles your project, the Babel plugin traverses the AST and replaces ENV.xxx member expressions:

your-code.ts (before bundling)
import { ENV } from 'varlock/env';
const url = ENV.API_URL;
const port = ENV.PORT;
const debug = ENV.DEBUG;
compiled output (after bundling)
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.

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.

.env.schema
# @generateTypes(lang='ts', path='env.d.ts')
# ---
# your config items...

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:

package.json
{
"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.

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:

.env.schema
# @defaultSensitive=false
# ---
NON_SECRET_FOO= # non-sensitive - will be inlined into bundle
# @sensitive
SECRET_KEY= # sensitive - will NEVER be inlined, runtime only

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.

app/secret+api.ts
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 });
}

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.

app/index.tsx
import { ENV } from 'varlock/env';
const url = ENV.API_URL; // ✅ non-sensitive — inlined at build time
const key = ENV.SECRET_KEY; // ❌ throws at runtime, build-time warning