Examples
Real-world configuration examples using popular schema libraries:
ArkType
Section titled “ArkType”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", }, },});Composition
Section titled “Composition”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),});Custom Discriminator
Section titled “Custom Discriminator”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, }, },});Env Files
Section titled “Env Files”Demonstrates envictus loading defaults from .env files using parseEnv().
import { defineConfig, parseEnv } from "envictus";import { z } from "zod";
// Example loading defaults from .env filesexport 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 }, },});Groups
Section titled “Groups”Demonstrates cascading sub-discriminators with groups. Each group is a regular defineConfig that resolves independently with its own discriminator and defaults, while inheriting the parent's mode as a fallback.
env.config.ts
import { defineConfig } from "envictus";import { z } from "zod";import { stripe } from "./stripe.env.config.js";
/** * Root application config that composes Stripe as a group. * * Groups resolve independently — each has its own discriminator and defaults. * When STRIPE_ENV is not set, the group cascades to APP_ENV's resolved mode. * * This means `APP_ENV=development envictus -- next dev` applies development * defaults everywhere, while `APP_ENV=development STRIPE_ENV=production envictus -- next dev` * uses production Stripe keys with development app settings. */export default defineConfig({ schema: z.object({ APP_ENV: z.enum(["development", "production"]).default("development"), PORT: z.coerce.number().min(1).max(65535), DATABASE_URL: z.string().url(), LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"), }), discriminator: "APP_ENV", defaults: { development: { PORT: 3000, DATABASE_URL: "postgres://localhost:5432/myapp_dev", LOG_LEVEL: "debug", }, production: { PORT: 8080, LOG_LEVEL: "warn", }, }, groups: { stripe },});stripe.env.config.ts
import { defineConfig } from "envictus";import { z } from "zod";
/** * Stripe environment configuration — works standalone or as a group. * * Uses its own discriminator (STRIPE_ENV) independent of the app's main * environment. When used as a group, falls back to the parent's mode * if STRIPE_ENV is not set. */export const stripe = defineConfig({ schema: z.object({ STRIPE_SECRET_KEY: z.string().min(1), STRIPE_WEBHOOK_SECRET: z.string().min(1), STRIPE_PUBLISHABLE_KEY: z.string().min(1), }), discriminator: "STRIPE_ENV", defaults: { development: { STRIPE_SECRET_KEY: "sk_test_placeholder", STRIPE_WEBHOOK_SECRET: "whsec_test_placeholder", STRIPE_PUBLISHABLE_KEY: "pk_test_placeholder", }, production: { // In production, all Stripe keys must come from the environment. // Setting undefined ensures schema defaults don't leak through. STRIPE_SECRET_KEY: undefined, STRIPE_WEBHOOK_SECRET: undefined, STRIPE_PUBLISHABLE_KEY: undefined, }, },});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", }, },});Valibot
Section titled “Valibot”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", }, },});