@howells/envy is the implemented foundation of Envy. It defines schemas, validates input objects, and returns typed parsed values.
import { defineEnv, v } from "@howells/envy";
import { z } from "zod";
export const envSchema = defineEnv({
server: {
DATABASE_URL: z.string().url(),
OPENAI_API_KEY: v(z.string().min(1), {
deploy: ["preview", "production"],
}),
},
public: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
system: {
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
CI: z.coerce.boolean().default(false),
},
optional: {
COHERE_API_KEY: z.string().min(1),
},
});Raw Zod schemas are enough for most variables. Use v(...) only when a variable needs metadata for tools.
Private values required by server-side code.
server: {
DATABASE_URL: z.string().url(),
}Missing values fail validation unless the Zod schema has a default or otherwise accepts undefined.
Client-safe values.
public: {
NEXT_PUBLIC_APP_URL: z.string().url(),
}Public keys must start with the configured public prefix. The default is NEXT_PUBLIC_.
defineEnv(
{
public: {
PUBLIC_APP_URL: z.string().url(),
},
},
{
publicPrefix: "PUBLIC_",
},
);Runtime or provider-owned values such as NODE_ENV, CI, VERCEL_URL, or RAILWAY_*.
system: {
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
}These values parse normally and are excluded from deploy pushes by default.
Optional integration values.
optional: {
COHERE_API_KEY: z.string().min(1),
}Missing and empty values parse as undefined. Present values must pass the Zod schema.
Parses all groups and returns a frozen object.
const env = envSchema.parse(process.env);Use this in non-Next server contexts and scripts.
Parses server, public, system, and optional.
export const env = envSchema.parseServer(process.env);Use this for server-side application code.
Parses only public.
export const env = envSchema.parseClient({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
});Use this for client bundle entrypoints.
Returns a proxy that validates each declared key on access.
const env = envSchema.lazy(process.env);Lazy mode is for awkward runtimes where validating everything up front is not viable. Prefer explicit parsing otherwise.
By default, Envy converts "" to undefined before validation.
This makes .env placeholders behave sensibly:
OPTIONAL_KEY=
PORT=defineEnv({
optional: {
OPTIONAL_KEY: z.string().min(1),
},
system: {
PORT: z.coerce.number().default(3000),
},
});OPTIONAL_KEY becomes undefined, and PORT uses its Zod default.
Disable this only when empty string is a meaningful value:
defineEnv(
{
server: {
EMPTY_ALLOWED: z.literal(""),
},
},
{
emptyStringAsUndefined: false,
},
);Parsed output only contains schema-declared keys.
const env = envSchema.parse({
DATABASE_URL: "https://db.example.com",
RANDOM_EXTRA: "ignored",
});RANDOM_EXTRA is stripped from the returned object.
CLI checks report undeclared keys in env files. Runtime parsing keeps application code focused on the declared typed surface.
Invalid input throws EnvValidationError.
import { EnvValidationError } from "@howells/envy";
try {
envSchema.parse(process.env);
} catch (error) {
if (error instanceof EnvValidationError) {
console.error(error.issues);
}
}Each issue includes:
keygroupmessagepath
This fails immediately when the schema is defined:
defineEnv({
public: {
APP_URL: z.string().url(),
},
});Use NEXT_PUBLIC_APP_URL or configure a different prefix.
A key may be declared in only one group.
defineEnv({
server: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
public: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
});This fails during schema definition.