Agent Skills: Expo Environment Configuration

This skill should be used when creating, modifying, or accessing environment variables in this Expo/React Native codebase. It enforces type-safe, validated environment configuration using Zod schemas. Use this skill when adding new environment variables, setting up env validation, or writing code that reads from process.env.

UncategorizedID: codyswanngt/lisa/expo-env-config

Install this agent skill to your local

pnpm dlx add-skill https://github.com/CodySwannGT/lisa/tree/HEAD/plugins/lisa-expo/skills/expo-env-config

Skill Files

Browse the full folder contents for expo-env-config.

Download Skill

Loading file tree…

plugins/lisa-expo/skills/expo-env-config/SKILL.md

Skill Metadata

Name
expo-env-config
Description
This skill should be used when creating, modifying, or accessing environment variables in this Expo/React Native codebase. It enforces type-safe, validated environment configuration using Zod schemas. Use this skill when adding new environment variables, setting up env validation, or writing code that reads from process.env.

Expo Environment Configuration

Overview

This skill enforces type-safe, validated environment variable management for Expo/React Native using Zod schemas. Environment variables are validated at build time and provide full TypeScript inference, regardless of their source (.env files, EAS Build secrets, CI/CD pipelines, or command-line exports).

Why This Pattern?

Unlike NestJS's @nestjs/config, Expo has no official type-safe env solution. The Zod validation pattern provides:

  1. Type safety - Full TypeScript inference via z.infer<typeof schema>
  2. Build-time validation - Fails fast with clear error messages before deployment
  3. Source agnostic - Works with .env, EAS secrets, CI variables
  4. Testing support - Easy mocking via module aliases

Core Pattern

Environment Schema (src/lib/env.ts)

import { z } from "zod";

/**
 * Environment variable schema with Zod validation.
 * Variables are validated at module load time.
 */
const envSchema = z.object({
  // Required variables
  EXPO_PUBLIC_API_URL: z.string().url(),
  EXPO_PUBLIC_APP_ENV: z.enum(["development", "staging", "production"]),

  // Optional variables with defaults
  EXPO_PUBLIC_SENTRY_DSN: z.string().optional(),
  EXPO_PUBLIC_FEATURE_FLAG: z
    .string()
    .transform(v => v === "true")
    .default("false"),
});

/**
 * Validated environment configuration.
 * Throws at module load if validation fails.
 */
export const env = envSchema.parse(process.env);

/**
 * Type-safe environment configuration.
 */
export type Env = z.infer<typeof envSchema>;

Usage in Components/Hooks

import { env } from "@/lib/env";

// Full autocomplete and type safety
const apiUrl = env.EXPO_PUBLIC_API_URL; // string
const isDev = env.EXPO_PUBLIC_APP_ENV === "development"; // boolean

Build-Time Validation (app.config.ts)

For variables needed during the build process, validate in app.config.ts:

// app.config.ts
const { z } = require("zod");

const buildEnvSchema = z.object({
  EXPO_PUBLIC_API_URL: z.string().url(),
  EXPO_PUBLIC_APP_ENV: z.enum(["development", "staging", "production"]),
  // Build-only secrets (not exposed to client)
  SENTRY_AUTH_TOKEN: z.string().optional(),
});

// Throws during `eas build` if invalid
const env = buildEnvSchema.parse(process.env);

module.exports = {
  name: "MyApp",
  slug: "my-app",
  extra: {
    apiUrl: env.EXPO_PUBLIC_API_URL,
    appEnv: env.EXPO_PUBLIC_APP_ENV,
  },
};

Variable Sources

Environment variables arrive in process.env from multiple sources:

| Source | When Available | How Set | |--------|----------------|---------| | .env.local | Local dev | Expo CLI auto-loads | | .env.development | Local dev | Copied to .env.local via npm script | | eas.json env | EAS Build | build.production.env section | | EAS Secrets | EAS Build | eas secret:create | | CI Variables | CI builds | GitHub Actions / GitLab CI settings |

The Zod pattern validates process.env directly - it doesn't care how variables got there.

Testing Pattern

Jest Setup (jest.setup.local.ts)

// Mock the env module for all tests
jest.mock("@/lib/env", () => ({
  env: {
    EXPO_PUBLIC_API_URL: "https://test.example.com",
    EXPO_PUBLIC_APP_ENV: "development",
    EXPO_PUBLIC_SENTRY_DSN: undefined,
    EXPO_PUBLIC_FEATURE_FLAG: false,
  },
}));

Override in Specific Tests

import { env } from "@/lib/env";

jest.mock("@/lib/env");

describe("ProductionFeature", () => {
  beforeEach(() => {
    (env as jest.Mocked<typeof env>).EXPO_PUBLIC_APP_ENV = "production";
  });

  it("should behave differently in production", () => {
    // Test production-specific behavior
  });
});

ESLint Enforcement

This pattern is enforced by ESLint's no-restricted-syntax rule in eslint.config.mjs:

"no-restricted-syntax": [
  "error",
  {
    selector: "MemberExpression[object.name='process'][property.name='env']",
    message: "Direct process.env access is forbidden. Import { env } from '@/lib/env' instead.",
  },
],

Exceptions (files allowed to use process.env):

  • lib/env.ts - The env validation module itself
  • app.config.ts - Expo build config
  • codegen.ts - GraphQL codegen config
  • playwright.config.ts - E2E test config
  • lighthouserc.js - Lighthouse CI config

Core Rules

1. Always Prefix with EXPO_PUBLIC_

Variables without this prefix are not available in client code:

// CORRECT - available in client
EXPO_PUBLIC_API_URL=https://api.example.com

// INCORRECT - only available at build time
API_URL=https://api.example.com

2. Never Access process.env Directly

Always use the validated env object:

// CORRECT - type-safe, validated
import { env } from "@/lib/env";
const url = env.EXPO_PUBLIC_API_URL;

// INCORRECT - untyped, unvalidated
const url = process.env.EXPO_PUBLIC_API_URL;

3. Validate Early, Fail Fast

Validation happens at module load. If a required variable is missing, the app fails immediately with a clear error rather than at runtime.

4. Use Transforms for Non-String Types

Environment variables are always strings. Use Zod transforms:

const envSchema = z.object({
  // Boolean from string
  EXPO_PUBLIC_DEBUG: z
    .string()
    .transform(v => v === "true")
    .default("false"),

  // Number from string
  EXPO_PUBLIC_TIMEOUT_MS: z
    .string()
    .transform(v => parseInt(v, 10))
    .default("5000"),

  // Array from comma-separated string
  EXPO_PUBLIC_ALLOWED_HOSTS: z
    .string()
    .transform(v => v.split(",").map(s => s.trim()))
    .default(""),
});

5. Separate Client vs Build-Only Variables

Keep sensitive build-time variables out of the client schema:

// Client variables (embedded in JS bundle)
const clientSchema = z.object({
  EXPO_PUBLIC_API_URL: z.string().url(),
});

// Build-only variables (NOT in bundle)
const buildSchema = z.object({
  SENTRY_AUTH_TOKEN: z.string(),
  EAS_PROJECT_ID: z.string(),
});

File Organization

src/
  lib/
    env.ts              # Main env schema and exports
app.config.ts           # Build-time validation (if needed)
.env.localhost          # Local development (git-ignored)
.env.development        # Development environment
.env.staging            # Staging environment
.env.production         # Production environment

Detailed Reference

For comprehensive patterns, transforms, and testing examples:

Anti-Patterns to Avoid

Never use process.env directly in components

// WRONG - untyped, could be undefined
const Component = () => {
  const url = process.env.EXPO_PUBLIC_API_URL;
  // url is string | undefined, no validation
};

// CORRECT - validated and typed
import { env } from "@/lib/env";
const Component = () => {
  const url = env.EXPO_PUBLIC_API_URL;
  // url is string, guaranteed to be valid URL
};

Never skip validation for "simple" variables

// WRONG - skipping validation
export const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:3000";

// CORRECT - always validate
const envSchema = z.object({
  EXPO_PUBLIC_API_URL: z.string().url().default("http://localhost:3000"),
});
export const { EXPO_PUBLIC_API_URL: API_URL } = envSchema.parse(process.env);

Never store secrets in EXPO_PUBLIC_ variables

// WRONG - secrets exposed in client bundle
EXPO_PUBLIC_API_SECRET=super-secret-key

// CORRECT - secrets only at build time, passed securely
SENTRY_AUTH_TOKEN=secret  # Build-only, not in bundle

Validation Checklist

When adding or modifying environment variables:

  • [ ] Variable is prefixed with EXPO_PUBLIC_ (if needed in client code)
  • [ ] Variable is added to the Zod schema in src/lib/env.ts
  • [ ] Appropriate Zod type/transform is used (url, enum, boolean transform, etc.)
  • [ ] Default value provided for optional variables
  • [ ] Jest mock updated in jest.setup.local.ts
  • [ ] Variable documented in .env.example or .env.development
  • [ ] Sensitive values use EAS Secrets, not .env files