From lisa-expo
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.
npx claudepluginhub codyswanngt/lisa --plugin lisa-expoThis skill uses the workspace's default tool permissions.
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).
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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).
Unlike NestJS's @nestjs/config, Expo has no official type-safe env solution. The Zod validation pattern provides:
z.infer<typeof schema>.env, EAS secrets, CI variablessrc/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>;
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
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,
},
};
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.
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,
},
}));
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
});
});
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 itselfapp.config.ts - Expo build configcodegen.ts - GraphQL codegen configplaywright.config.ts - E2E test configlighthouserc.js - Lighthouse CI configVariables 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
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;
Validation happens at module load. If a required variable is missing, the app fails immediately with a clear error rather than at runtime.
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(""),
});
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(),
});
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
For comprehensive patterns, transforms, and testing examples:
// 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
};
// 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);
// 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
When adding or modifying environment variables:
EXPO_PUBLIC_ (if needed in client code)src/lib/env.tsjest.setup.local.ts.env.example or .env.development.env files