> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vertz.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Environment Variables

> Validate and type-check environment variables with createEnv()

`createEnv()` validates environment variables at startup using a `@vertz/schema` schema. If any variable is missing or invalid, the server fails immediately with a clear error — not 30 minutes later when a request hits an undefined config value.

## Why not `process.env`?

`process.env.PORT` is `string | undefined`. A typo in the variable name is a silent `undefined`. A missing `.env` file is a runtime surprise. There's no type safety, no validation, and no way for TypeScript to help you.

```ts theme={null}
// Dangerous — silent failures
const port = Number(process.env.PROT) || 3000; // Typo: PROT instead of PORT
const dbUrl = process.env.DATABASE_URL!; // Crashes at first query, not at startup
```

`createEnv()` catches both issues at startup:

```ts theme={null}
import { createEnv } from '@vertz/server';
import { s } from '@vertz/schema';

const env = createEnv({
  schema: s.object({
    PORT: s.coerce.number().default(3000),
    DATABASE_URL: s.string(),
  }),
});

// env.PORT — number, guaranteed
// env.DATABASE_URL — string, guaranteed
// env.PROT — TypeScript error, caught at compile time
```

## Basic usage

```ts theme={null}
// src/api/env.ts
import { createEnv } from '@vertz/server';
import { s } from '@vertz/schema';

export const env = createEnv({
  schema: s.object({
    PORT: s.coerce.number().default(3000),
    DATABASE_URL: s.string(),
    NODE_ENV: s.enum(['development', 'production', 'test']).default('development'),
  }),
});
```

Then import it wherever you need config:

```ts theme={null}
// src/api/server.ts
import { createServer } from '@vertz/server';
import { env } from './env';

const app = createServer({
  /* ... */
});

if (import.meta.main) {
  app.listen(env.PORT);
}
```

`import.meta.main` is `true` when the file is executed directly (`vtz src/api/server.ts`) and `false` when it's imported by another module — for example, a test that imports `app` to make in-process requests. This is the standard "run if main" idiom. The vtz runtime sets it natively; the `boolean` type comes from `vertz/client`, which is already in the scaffolded `tsconfig.json`.

## Loading `.env` files

Use the `load` property to read variables from dotenv files:

```ts theme={null}
// src/api/env.ts
import { createEnv } from '@vertz/server';
import { s } from '@vertz/schema';

export const env = createEnv({
  load: ['.env', '.env.local'],
  schema: s.object({
    PORT: s.coerce.number().default(3000),
    DATABASE_URL: s.string(),
    API_SECRET: s.string(),
  }),
});
```

Files are loaded in order — later files override earlier ones. This lets you keep shared defaults in `.env` and developer-specific overrides in `.env.local`.

### Precedence

| Source                         | Priority | Use case                          |
| ------------------------------ | -------- | --------------------------------- |
| `process.env`                  | Lowest   | CI/CD pipelines, system-level env |
| `.env` files (in `load` order) | Medium   | Project defaults, local overrides |
| Explicit `env` record          | Highest  | Edge runtimes, tests              |

Loaded files **override** `process.env`. If `.env` sets `PORT=4000` and `process.env.PORT` is `3000`, the result is `4000`. This means your `.env` files are the source of truth for local development.

### Missing files

Missing files are silently skipped. This is intentional — `.env.local` often doesn't exist in CI, and that shouldn't break your build:

```ts theme={null}
createEnv({
  load: ['.env', '.env.local'], // .env.local may not exist — that's fine
  schema: s.object({ PORT: s.coerce.number().default(3000) }),
});
```

### Common file patterns

```ts theme={null}
// Development
load: ['.env', '.env.local'];

// Environment-specific
load: ['.env', `.env.${process.env.NODE_ENV}`, '.env.local'];
```

Add `.env.local` to `.gitignore` — it's for secrets that shouldn't be committed.

## How it works

1. Loads variables from `.env` files (if `load` is specified)
2. Merges with `process.env` (files override process env)
3. Applies explicit `env` record on top (if provided)
4. Validates every variable against the schema
5. Returns a **frozen, immutable** object — typed from the schema
6. Throws at startup if validation fails

### Startup error example

If `DATABASE_URL` is missing and has no default:

```
Environment validation failed:
  DATABASE_URL: Required
```

The server never starts. No ambiguity about what's wrong.

## Coercion

Environment variables are always strings. Use `s.coerce.*` to parse them into the right types:

```ts theme={null}
import { createEnv } from '@vertz/server';
import { s } from '@vertz/schema';

createEnv({
  schema: s.object({
    PORT: s.coerce.number().default(3000), // "3000" → 3000
    ENABLE_CACHE: s.coerce.boolean().default(true), // "true" → true
    MAX_RETRIES: s.coerce.number().int().min(0),
  }),
});
```

## Edge runtimes

On Cloudflare Workers, environment variables are passed as context bindings — not available in `process.env`. Pass them explicitly:

```ts theme={null}
// worker.ts
import { createEnv } from '@vertz/server';
import { envSchema } from './env'; // your schema from the Basic usage example

export default {
  async fetch(request: Request, workerEnv: Env) {
    const env = createEnv({
      schema: envSchema,
      env: workerEnv, // Use worker bindings instead of process.env
    });
    // ...
  },
};
```

## Testing

Inject controlled values for deterministic tests:

```ts theme={null}
import { createEnv } from '@vertz/server';
import { s } from '@vertz/schema';

const env = createEnv({
  schema: s.object({
    PORT: s.coerce.number(),
    API_KEY: s.string(),
  }),
  env: {
    PORT: '4000',
    API_KEY: 'test-key',
  },
});

expect(env.PORT).toBe(4000);
expect(env.API_KEY).toBe('test-key');
```

## Immutability

The returned object is deeply frozen. Attempts to mutate it throw at runtime:

```ts theme={null}
env.PORT = 9999; // TypeError: Cannot assign to read only property
```

This prevents accidental mutation of configuration values across your application.

## Pattern: single env module

Define one `env.ts` file per service and import it everywhere. Don't scatter `createEnv()` calls across multiple files:

```
src/api/
  env.ts       ← single source of truth for env
  server.ts    ← imports env
  db.ts        ← imports env
```

This gives you one place to see all required environment variables and their types.
