# T3 Env Validation

> Type-safe environment variable validation with @t3-oss/env-nextjs and Zod. Build-time validation ensures all required env vars are present and correctly typed.

- Skill: `janszewczyk/t3-env-validation` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add janszewczyk/t3-env-validation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/janszewczyk/t3-env-validation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: JanSzewczyk (https://skillmd.com/u/janszewczyk)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/janszewczyk/t3-env-validation

---


# T3 Env Validation Skill

Type-safe environment variable validation with @t3-oss/env-nextjs and Zod.

> **Reference Files:**
>
> - [setup.md](./references/setup.md) - Configuration patterns
> - [schema-patterns.md](./references/schema-patterns.md) - Common Zod validation patterns
> - [testing.md](./references/testing.md) - Mocking env vars in tests
> - [examples.md](./references/examples.md) - Practical examples

## Project Configuration

Environment variables are validated in two files:

| File                 | Purpose          | Variables                          |
| -------------------- | ---------------- | ---------------------------------- |
| `data/env/server.ts` | Server-only vars | `CLERK_SECRET_KEY`, Firebase, etc. |
| `data/env/client.ts` | Public vars      | `NEXT_PUBLIC_*` variables          |

## Quick Start

### Adding Server Environment Variable

```typescript
// data/env/server.ts
import { z } from "zod";
import { createEnv } from "@t3-oss/env-nextjs";

export const env = createEnv({
  server: {
    // Add new variable
    DATABASE_URL: z.string().url(),
    API_SECRET: z.string().min(32),
  },
  experimental__runtimeEnv: process.env,
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
  emptyStringAsUndefined: true,
});
```

### Adding Client Environment Variable

```typescript
// data/env/client.ts
import { z } from "zod";
import { createEnv } from "@t3-oss/env-nextjs";

export const env = createEnv({
  client: {
    // Must be prefixed with NEXT_PUBLIC_
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  // Must manually map client vars
  experimental__runtimeEnv: {
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
  },
  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
  emptyStringAsUndefined: true,
});
```

### Usage in Code

```typescript
// Server-side (Server Components, Server Actions, API routes)
import { env } from "~/data/env/server";

const apiSecret = env.API_SECRET; // Type-safe!

// Client-side (Client Components)
import { env } from "~/data/env/client";

const apiUrl = env.NEXT_PUBLIC_API_URL; // Type-safe!
```

## Key Concepts

### Build-Time Validation

T3 Env validates environment variables at build time:

```
npm run build

❌ Invalid environment variables:
  CLERK_SECRET_KEY: Required
  FIREBASE_PROJECT_ID: Required
```

This prevents deploying with missing configuration.

### Server vs Client Variables

| Type   | Prefix         | Accessible In                   | Bundled            |
| ------ | -------------- | ------------------------------- | ------------------ |
| Server | None           | Server Components, Actions, API | No                 |
| Client | `NEXT_PUBLIC_` | Everywhere                      | Yes (in JS bundle) |

**Security Rule:** Never put secrets in client variables!

### Empty String Handling

```typescript
emptyStringAsUndefined: true;
```

With this setting:

- `VAR=""` is treated as undefined
- Required vars with empty string will fail validation
- Optional vars with empty string use default

## Common Patterns

### Required String

```typescript
API_KEY: z.string();
```

### Optional with Default

```typescript
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"])
  .optional()
  .default("info");
```

### Boolean Transform

```typescript
FEATURE_FLAG: z.enum(["true", "false"])
  .optional()
  .transform((val) => val === "true");
```

### URL Validation

```typescript
API_URL: z.string().url();
DATABASE_URL: z.string().url().startsWith("postgresql://");
```

### Number Transform

```typescript
PORT: z.string().transform((val) => parseInt(val, 10));
TIMEOUT_MS: z.coerce.number().int().positive();
```

## File Locations

| Purpose      | Location             |
| ------------ | -------------------- |
| Server env   | `data/env/server.ts` |
| Client env   | `data/env/client.ts` |
| Env template | `.env.example`       |
| Local env    | `.env.local`         |

## Troubleshooting

### Build Fails with Missing Env

```bash
# Skip validation (for Docker builds)
SKIP_ENV_VALIDATION=true npm run build
```

### Variable Not Available

1. Server var → Can only be used in server code
2. Client var → Must be in `experimental__runtimeEnv`
3. Check spelling and `NEXT_PUBLIC_` prefix

### Type Errors

```typescript
// Import from correct file
import { env } from "~/data/env/server"; // For server vars
import { env } from "~/data/env/client"; // For client vars
```

## Related Skills

- `server-actions` - Using env vars in server actions
- `firebase-firestore` - Firebase env configuration
- `clerk-auth-proxy` - Clerk env configuration

