Skip to content

Examples

Real-world configuration examples using popular schema libraries:

Demonstrates envictus with ArkType for schema validation.

/**
* ArkType example - uses string-based type expressions for concise schema definitions.
* - Inline defaults: `= 'value'` syntax (e.g., `"'dev' | 'prod' = 'dev'"`)
* - Optional fields: `"key?"` syntax (e.g., `"DEBUG?"`)
* - Built-in validators: `string.url`, `string.numeric`, etc.
*/
import { type } from "arktype";
import { defineConfig } from "envictus";
export default defineConfig({
schema: type({
NODE_ENV: "'development' | 'production' | 'test' = 'development'",
DATABASE_URL: "string.url",
PORT: "string.numeric",
"DEBUG?": "string",
LOG_LEVEL: "'debug' | 'info' | 'warn' | 'error' = 'info'",
}),
discriminator: "NODE_ENV",
defaults: {
development: {
DATABASE_URL: "postgres://localhost:5432/dev",
PORT: "3000",
DEBUG: "true",
LOG_LEVEL: "debug",
},
production: {
DATABASE_URL: "postgres://prod.example.com:5432/prod",
PORT: "8080",
DEBUG: "false",
LOG_LEVEL: "warn",
},
test: {
DATABASE_URL: "postgres://localhost:5432/test",
PORT: "3001",
DEBUG: "false",
LOG_LEVEL: "error",
},
},
});

Demonstrates splitting environment configuration into separate client and server configs, where the client schema serves as the shared base that the server extends via mergeDefaults.

client.env.config.ts

import { defineConfig } from "envictus";
import { z } from "zod";
// Client schema is the base — only browser-safe, prefixed variables.
// Frameworks like Next.js and Vite strip unprefixed vars from the client bundle,
// so this schema doubles as the shared foundation that the server config extends.
export const clientSchema = z.object({
NEXT_PUBLIC_APP_ENV: z.enum(["local", "staging", "production"]).default("local"),
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_SENTRY_DSN: z.string().optional(),
});
export const clientDefaults = {
local: {
NEXT_PUBLIC_API_URL: "http://localhost:3000/api",
},
staging: {
NEXT_PUBLIC_API_URL: "https://staging.api.example.com",
NEXT_PUBLIC_SENTRY_DSN: "https://abc@sentry.io/123",
},
production: {
NEXT_PUBLIC_API_URL: "https://api.example.com",
NEXT_PUBLIC_SENTRY_DSN: "https://abc@sentry.io/456",
},
};
export default defineConfig({
schema: clientSchema,
discriminator: "NEXT_PUBLIC_APP_ENV",
defaults: clientDefaults,
});

server.env.config.ts

import { defineConfig, mergeDefaults } from "envictus";
import { z } from "zod";
import { clientDefaults, clientSchema } from "./client.env.config.js";
// Server config extends the client schema with secrets and internal config.
// The server needs public vars too (e.g. API_URL for SSR, Sentry DSN for error reporting).
const serverSchema = z.object({
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
SESSION_SECRET: z.string().min(32),
PORT: z.coerce.number().min(1).max(65535),
});
const serverDefaults = {
local: {
LOG_LEVEL: "debug" as const,
DATABASE_URL: "postgres://localhost:5432/myapp_dev",
REDIS_URL: "redis://localhost:6379",
SESSION_SECRET: "local-secret-that-is-at-least-32-characters",
PORT: 3000,
},
staging: {
LOG_LEVEL: "info" as const,
PORT: 8080,
},
production: {
LOG_LEVEL: "warn" as const,
PORT: 8080,
},
};
export default defineConfig({
schema: clientSchema.merge(serverSchema),
discriminator: "NEXT_PUBLIC_APP_ENV",
defaults: mergeDefaults(clientDefaults, serverDefaults),
});

Demonstrates envictus with a custom discriminator variable instead of the default NODE_ENV.

import { defineConfig } from "envictus";
import { z } from "zod";
// Example with a custom discriminator (not NODE_ENV)
export default defineConfig({
schema: z.object({
APP_ENV: z.enum(["local", "staging", "prod"]).default("local"),
API_URL: z.string().url(),
API_KEY: z.string().min(1),
TIMEOUT_MS: z.coerce.number().positive().default(5000),
}),
discriminator: "APP_ENV",
defaults: {
local: {
API_URL: "http://localhost:4000",
API_KEY: "local-dev-key",
TIMEOUT_MS: 10000,
},
staging: {
API_URL: "https://staging.api.example.com",
API_KEY: "staging-key",
TIMEOUT_MS: 5000,
},
prod: {
API_URL: "https://api.example.com",
API_KEY: "prod-key",
TIMEOUT_MS: 3000,
},
},
});

Demonstrates envictus loading defaults from .env files using parseEnv().

import { defineConfig, parseEnv } from "envictus";
import { z } from "zod";
// Example loading defaults from .env files
export default defineConfig({
schema: z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
API_URL: z.string().url(),
API_KEY: z.string().min(1),
TIMEOUT_MS: z.coerce.number().positive().default(5000),
}),
discriminator: "NODE_ENV",
defaults: {
development: {
...parseEnv(".env.local", { onMissing: "ignore" }),
API_URL: "https://localhost:3000/api",
},
test: {
API_URL: "https://localhost:3000/api",
API_KEY: "test-key",
},
production: {
API_URL: "https://api.example.com",
// API_KEY required with no default in prod
},
},
});

Demonstrates envictus with Joi for schema validation.

/**
* Joi example - strict by default, requires explicit allowances for extra fields.
* - `.unknown()` on object allows extra env vars to pass through without errors
* - `.valid()` constrains to specific values (enum-like behavior)
* - Built-in validators: `.port()` (1-65535), `.uri()`, etc.
*/
import { defineConfig } from "envictus";
import Joi from "joi";
export default defineConfig({
schema: Joi.object({
NODE_ENV: Joi.string().valid("development", "production", "test").default("development"),
DATABASE_URL: Joi.string().uri().required(),
PORT: Joi.number().port().required(),
DEBUG: Joi.boolean().optional(),
LOG_LEVEL: Joi.string().valid("debug", "info", "warn", "error").default("info"),
}).unknown(),
discriminator: "NODE_ENV",
defaults: {
development: {
DATABASE_URL: "postgres://localhost:5432/dev",
PORT: 3000,
DEBUG: true,
LOG_LEVEL: "debug",
},
production: {
DATABASE_URL: "postgres://prod.example.com:5432/prod",
PORT: 8080,
DEBUG: false,
LOG_LEVEL: "warn",
},
test: {
DATABASE_URL: "postgres://localhost:5432/test",
PORT: 3001,
DEBUG: false,
LOG_LEVEL: "error",
},
},
});

Demonstrates envictus with Valibot for schema validation.

/**
* Valibot example - requires explicit transformation pipelines for type coercion.
* - `v.pipe()` chains validators/transformers (e.g., unknown -> transform -> validate)
* - `v.unknown()` accepts any input, allowing subsequent transform/validation
* - Boolean transform needed because env vars are strings ("true"/"1" -> boolean)
*/
import { defineConfig } from "envictus";
import * as v from "valibot";
export default defineConfig({
schema: v.object({
NODE_ENV: v.optional(v.picklist(["development", "production", "test"]), "development"),
DATABASE_URL: v.pipe(v.string(), v.url()),
PORT: v.pipe(v.unknown(), v.transform(Number), v.number(), v.minValue(1), v.maxValue(65535)),
DEBUG: v.optional(
v.pipe(
v.unknown(),
v.transform((val) => val === "true" || val === "1"),
),
),
LOG_LEVEL: v.optional(v.picklist(["debug", "info", "warn", "error"]), "info"),
}),
discriminator: "NODE_ENV",
defaults: {
development: {
DATABASE_URL: "postgres://localhost:5432/dev",
PORT: 3000,
DEBUG: true,
LOG_LEVEL: "debug",
},
production: {
DATABASE_URL: "postgres://prod.example.com:5432/prod",
PORT: 8080,
DEBUG: false,
LOG_LEVEL: "warn",
},
test: {
DATABASE_URL: "postgres://localhost:5432/test",
PORT: 3001,
DEBUG: false,
LOG_LEVEL: "error",
},
},
});

Demonstrates envictus with Yup for schema validation.

/**
* Yup example - has built-in coercion for common types (string -> number, string -> boolean).
* - `.required()` marks field as mandatory; `.optional()` allows undefined
* - `.oneOf()` constrains to specific values (enum-like behavior)
* - `.default()` provides fallback value if field is undefined
*/
import { defineConfig } from "envictus";
import * as yup from "yup";
export default defineConfig({
schema: yup.object({
NODE_ENV: yup.string().oneOf(["development", "production", "test"]).default("development"),
DATABASE_URL: yup.string().url().required(),
PORT: yup.number().min(1).max(65535).required(),
DEBUG: yup.boolean().optional(),
LOG_LEVEL: yup.string().oneOf(["debug", "info", "warn", "error"]).default("info"),
}),
discriminator: "NODE_ENV",
defaults: {
development: {
DATABASE_URL: "https://db.example.com/dev",
PORT: 3000,
DEBUG: true,
LOG_LEVEL: "debug",
},
production: {
DATABASE_URL: "https://db.example.com/prod",
PORT: 8080,
DEBUG: false,
LOG_LEVEL: "warn",
},
test: {
DATABASE_URL: "https://db.example.com/test",
PORT: 3001,
DEBUG: false,
LOG_LEVEL: "error",
},
},
});

Demonstrates envictus with Zod for schema validation.

import { defineConfig } from "envictus";
import { z } from "zod";
export default defineConfig({
schema: z.object({
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().min(1).max(65535),
DEBUG: z.coerce.boolean().optional(),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
}),
discriminator: "NODE_ENV",
defaults: {
// Defaults when NODE_ENV=development
development: {
DATABASE_URL: "postgres://localhost:5432/dev",
PORT: 3000,
DEBUG: true,
LOG_LEVEL: "debug",
},
// Defaults when NODE_ENV=production
production: {
DATABASE_URL: "postgres://prod.example.com:5432/prod",
PORT: 8080,
DEBUG: false,
LOG_LEVEL: "warn",
},
// Defaults when NODE_ENV=test
test: {
DATABASE_URL: "postgres://localhost:5432/test",
PORT: 3001,
DEBUG: false,
LOG_LEVEL: "error",
},
},
});