---
title: Expo / React Native
description: How to integrate varlock with Expo and React Native for secure, type-safe environment management
---
import SyncedCodeTabs from '@/components/SyncedCodeTabs.astro'
import { TabItem, Tabs, Steps } from "@astrojs/starlight/components";
import Badge from '@/components/Badge.astro';
import ExecCommandWidget from '@/components/ExecCommandWidget.astro';
import InstallJsDepsWidget from '@/components/InstallJsDepsWidget.astro';

<div class="page-badges">
  <Badge npmPackage="@varlock/expo-integration" />
</div>

Expo and [React Native CLI](https://github.com/react-native-community/cli) projects both use the [Metro bundler](https://metrobundler.dev/), 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.

Both stacks use the same [`@varlock/expo-integration`](https://npmx.dev/package/@varlock/expo-integration) package. Only the Babel preset and Metro base config differ between Expo and React Native CLI projects.

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. In Expo projects they are accessible at runtime only in [Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/) (`+api` files)
- Patches the global console to redact sensitive values from logs

No CLI command wrapping is needed. Env loads when Metro/Babel start. Run `expo start`, `npx react-native start`, or your usual build commands as normal.

## Setup

<Tabs syncKey="expo-rn">
  <TabItem label="Expo">
    :::caution[Requirements]
    - Node.js v22 or higher
    - Expo SDK v50 or higher
    :::
  </TabItem>
  <TabItem label="React Native CLI">
    :::caution[Requirements]
    - Node.js v22 or higher
    - React Native v0.72 or higher (ships `@react-native/metro-config`)
    :::
  </TabItem>
</Tabs>

<Steps>

1. **Install varlock and the integration package**
    <InstallJsDepsWidget packages="@varlock/expo-integration varlock" />

1. **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.

    <ExecCommandWidget command="varlock init" showBinary={false} />

1. **Add the Babel plugin to your `babel.config.js`**

    <Tabs syncKey="expo-rn">
      <TabItem label="Expo">
        ```diff lang="js" title="babel.config.js"
        module.exports = {
          presets: ['babel-preset-expo'],
        +  plugins: [
        +    require('@varlock/expo-integration/babel-plugin'),
        +  ],
        };
        ```
      </TabItem>
      <TabItem label="React Native CLI">
        ```diff lang="js" title="babel.config.js"
        module.exports = {
          presets: ['module:@react-native/babel-preset'],
        +  plugins: [
        +    require('@varlock/expo-integration/babel-plugin'),
        +  ],
        };
        ```
      </TabItem>
    </Tabs>

1. **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.

    <Tabs syncKey="expo-rn">
      <TabItem label="Expo">
        ```diff lang="js" title="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);
        ```
      </TabItem>
      <TabItem label="React Native CLI">
        ```diff lang="js" title="metro.config.js"
        const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
        +const { withVarlockMetroConfig } = require('@varlock/expo-integration/metro-config');

        const config = mergeConfig(getDefaultConfig(__dirname), {});

        -module.exports = config;
        +module.exports = withVarlockMetroConfig(config);
        ```
      </TabItem>
    </Tabs>

    :::tip[What does this do?]
    - Installs a custom resolver so that `import { ENV } from 'varlock/env'` works (Metro doesn't support `package.json` `"exports"` subpaths by default).
    - Initializes the varlock `ENV` proxy in the main Metro process. Metro forks worker processes for Babel transforms; the wrapper ensures the environment is initialized in the main process (needed for Expo Router `+api` server routes and dev-time console redaction).
    :::

</Steps>

---

## Accessing environment variables

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

```ts title="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
```

#### 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

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

```ts title="your-code.ts (before bundling)"
import { ENV } from 'varlock/env';
const url = ENV.API_URL;
const port = ENV.PORT;
const debug = ENV.DEBUG;
```

```ts title="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. In Expo Router `+api` server routes they resolve at runtime via the proxy; in all other code they throw at runtime.

### Type-safety and IntelliSense

To enable type-safety and IntelliSense for your env vars, enable the [`@generateTsTypes` root decorator](/reference/root-decorators/#generatetstypes) in your `.env.schema`. Note that if your schema was created using `varlock init`, it will include this by default.

```diff lang="env-spec" title=".env.schema"
+# @generateTsTypes(path='env.d.ts')
# ---
# your config items...
```

:::caution[TypeScript config]
If you find you are not getting type completion on `ENV`, you may need to add your generated type files (usually `env.d.ts`) to your `tsconfig.json`'s `include` array.
:::

---

## 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](/reference/root-decorators/#currentenv).

Usually this env var will be defaulted to something like `development` in your `.env.schema` file, and you can override it when running commands.

<Tabs syncKey="expo-rn">
  <TabItem label="Expo">
    For Expo projects, set it in your `package.json` scripts or via [EAS environment variables](https://docs.expo.dev/eas/environment-variables/):

    ```diff lang="json" title="package.json" add=/APP_ENV=[a-z]+/
    {
      "scripts": {
        "start": "expo start",
        "build:staging": "APP_ENV=staging eas build",
        "build:production": "APP_ENV=production eas build",
      }
    }
    ```
  </TabItem>
  <TabItem label="React Native CLI">
    For React Native CLI projects, set it in your `package.json` scripts:

    ```diff lang="json" title="package.json" add=/APP_ENV=[a-z]+/
    {
      "scripts": {
        "start": "react-native start",
        "start:staging": "APP_ENV=staging react-native start",
        "android:staging": "APP_ENV=staging react-native run-android",
        "ios:staging": "APP_ENV=staging react-native run-ios",
      }
    }
    ```
  </TabItem>
</Tabs>

See the [environments guide](/guides/environments) for more information.

## 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`](/reference/root-decorators/#defaultsensitive) root decorator and the [`@sensitive`](/reference/item-decorators/#sensitive) item decorator. See the [secrets guide](/guides/secrets) for more information.

Set a default and explicitly mark items:

```diff lang="env-spec" title=".env.schema" add="@sensitive"
+# @defaultSensitive=false
# ---
NON_SECRET_FOO= # non-sensitive - will be inlined into bundle
# @sensitive
SECRET_KEY=     # sensitive - will NEVER be inlined, runtime only
```

:::caution[Bundle security]
Non-sensitive items are inlined into your JavaScript bundle. Anyone who extracts your app's bundle can read these values. **Never** mark credentials, tokens, or other secrets as non-sensitive.
:::

### Sensitive values in server routes (Expo only)

:::note[Expo Router only]
[Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/) (`+api` files) are an **Expo-only** feature. React Native CLI apps have no server-side JS runtime, so skip to the next section.
:::

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.

```ts title="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 });
}
```

### Sensitive values in native code

In native app code (anything that isn't an Expo Router `+api` server route), sensitive values are **not available**. React Native apps run entirely on the device, so 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. This applies to **all** React Native code, including React Native CLI projects where every screen and component is client-side.

```ts title="src/screens/Home.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
```

:::note[Expo pages are universal]
Unlike Next.js Server Components, Expo Router page components run on **both** the server (SSR) and the client (hydration). This means you cannot safely access sensitive values in page components. Use `+api` server routes instead.
:::

---

## Reference
- [Root decorators reference](/reference/root-decorators)
- [Item decorators reference](/reference/item-decorators)
- [Functions reference](/reference/functions)
- [Expo environment variables docs](https://docs.expo.dev/guides/environment-variables/)
- [Expo Router API routes](https://docs.expo.dev/router/reference/api-routes/)
- [React Native Metro docs](https://reactnative.dev/docs/metro)
- [React Native Community CLI](https://github.com/react-native-community/cli)
- [Metro bundler docs](https://metrobundler.dev/)
