plugins/lisa-expo-agy/skills/expo-env-config/SKILL.md
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 skillsauth add codyswanngt/lisa expo-env-configInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 filesdocumentation
Onboard a user to the project via its LLM Wiki. Interviews the user about themselves in relation to the project, captures that to project-scoped memory only, then gives a guided tour of what the project is and sample questions they can ask. Use when someone is new to the project or asks to be onboarded. Read-mostly — it does not open PRs or write PII into the wiki.
documentation
Migrate an existing, hand-rolled wiki implementation onto the lisa-wiki kernel — phased and compatibility-first, with a strict no-loss guarantee. Use when adopting lisa-wiki in a repo that already has its own wiki/, ingest skills, docs, or roles. Renaming things into the canonical shape is fine; losing functionality or data is not. Ends by running /doctor.
development
Health-check the LLM Wiki. Reports orphan pages, contradictions, stale claims, broken internal links, missing index/log coverage, structure-manifest violations, and secret/tenant leaks. Use periodically or before hardening a wiki. Read-only — it reports findings, it does not fix them.
testing
Ingest source material into the LLM Wiki. With an argument (URL, file path, or prompt) it ingests that one source; with no argument it runs a full ingest across every enabled non-external-write source. Routes to the right connector, then runs the ordered pipeline (source note → synthesis → index → log → verify → state → commit/PR). Use whenever new knowledge should enter the wiki.