---
title: Schema definition and field types
description: Define your environment contract with defineEnv and the env builder: 10 field types with chain methods for defaults, secrets, and documentation.
url: https://pr-1-7abef52380a8.thally.app/envlock-schema-definition
---

# Schema definition and field types

Define your environment contract with defineEnv and the env builder: 10 field types with chain methods for defaults, secrets, and documentation.

## Defining a schema

Use `defineEnv` to declare the environment variables your app needs. Each key is
a variable name; each value is a field created from the `env` builder:

```js
import { defineEnv, env } from "@envlock/core";

export default defineEnv({
  NODE_ENV: env.enum(["development", "test", "production"]).default("development"),
  PORT: env.port().default(3000).describe("HTTP listen port"),
  DATABASE_URL: env.url({ protocols: ["postgres:", "postgresql:"] })
    .secret()
    .describe("Primary database connection string"),
  SESSION_SECRET: env.string().secret(),
  REQUEST_TIMEOUT: env.duration().default(30_000),
  ALLOWED_ORIGINS: env.list().default(["http://localhost:3000"]),
  LOG_LEVEL: env.enum(["debug", "info", "warn", "error"]).default("info"),
  FEATURE_FLAGS: env.json().optional(),
});
```

Variable names must match `/^[A-Za-z_][A-Za-z0-9_]*$/`; `defineEnv` throws a
`TypeError` at definition time if any key is invalid.

The returned `EnvSchema` is a frozen object with `kind: "envlock.schema"`,
`shape` (the field map), and `keys` (in declaration order).

## Type inference

`Infer<typeof schema>` derives a fully typed record from the schema. Fields
marked `.optional()` become optional properties; fields with `.default()` stay
required in the output type:

```ts
import { defineEnv, env, type Infer } from "@envlock/core";

const schema = defineEnv({
  PORT: env.port().default(3000),
  DEBUG: env.boolean().optional(),
});

type Config = Infer<typeof schema>;
// { readonly PORT: number; readonly DEBUG?: boolean }
```

## Field types

Every builder returns a required, non-secret field. Chain `.optional()`,
`.default(value)`, `.secret()`, `.describe(text)`, and `.example(text)` to
refine it; each call returns a new immutable field.

| Builder | Accepted input | Output type | Notes |
| --- | --- | --- | --- |
| `env.string()` | any string | `string` | Passed through verbatim (no trimming). |
| `env.number()` | `"3.5"`, `" -2 "` | `number` | Must be finite; blank is rejected. |
| `env.integer()` | `"42"` | `number` | Safe integers only (`Number.isSafeInteger`). |
| `env.boolean()` | `true/false`, `1/0`, `yes/no`, `on/off` (case-insensitive) | `boolean` | |
| `env.port()` | `"1"` ... `"65535"` | `number` | Integer in range 1-65535. |
| `env.url(options?)` | `"https://example.com"` | `string` | Must parse with `new URL()`; optional `protocols` allow-list (e.g. `["https:"]`). |
| `env.enum(values)` | one of the members | literal union | Members are inferred as a literal union type. |
| `env.json<T>()` | any JSON document | `T` (default `unknown`) | `JSON.parse`; syntax errors become `invalid` issues. |
| `env.duration()` | `"250ms"`, `"30s"`, `"5m"`, `"2h"`, `"1d"`, `"100"` | `number` (milliseconds) | A bare number is taken as milliseconds. Supported units: `ms`, `s`, `m`, `h`, `d`. |
| `env.list(options?)` | `"a, b ,,c"` | `string[]` | Items trimmed, empty items dropped; comma separator by default (configurable via `separator` option). |

## Chain methods

| Method | Effect |
| --- | --- |
| `.optional()` | Marks the field as optional. Absent values produce no issue; the output type becomes `T \| undefined`. |
| `.default(value)` | Supplies a fallback for absent values. The field stays required in the output type. |
| `.secret()` | Masks the raw value in all output: issue messages, `.env.example`, `describeSchema()`, and `redact()`. |
| `.describe(text)` | Sets a human-readable description, rendered into `.env.example` as a comment. |
| `.example(text)` | Sets the sample value shown in `.env.example` (never used for secret fields). |

Each chain method returns a new frozen `Field` — the original is never mutated,
so fields can be shared safely between schemas.

## Config file for the CLI

The CLI and MCP server look for `envlock.config.mjs` or `envlock.config.js` in
the working directory, or accept an explicit `--schema <path>`. The file must
use one of these export forms:

```js
// Default export (preferred)
export default defineEnv({ ... });

// Named export
export const schema = defineEnv({ ... });
```

Run `npx envlock init` to generate a starter config file.