Envy v1 is Next-first. The default public prefix is NEXT_PUBLIC_, and the recommended layout separates schema, server env, and client env.
src/env/schema.ts
src/env/server.ts
src/env/client.ts// src/env/schema.ts
import { defineEnv } from "@howells/envy";
import { z } from "zod";
export const envSchema = defineEnv({
server: {
DATABASE_URL: z.string().url(),
OPENAI_API_KEY: z.string().min(1),
},
public: {
NEXT_PUBLIC_APP_URL: z.string().url(),
},
system: {
NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
},
});// src/env/server.ts
import { envSchema } from "./schema";
export const env = envSchema.parseServer(process.env);Server parsing includes server, public, system, and optional keys.
// src/env/client.ts
import { envSchema } from "./schema";
export const env = envSchema.parseClient({
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
});Client parsing includes only public keys.
Next.js expects public env access to be statically visible:
process.env.NEXT_PUBLIC_APP_URLA dynamic loop over schema keys risks values not being inlined into the client bundle. syncNextEnv() generates literal mappings instead of relying on runtime reflection.
import { syncNextEnv } from "@howells/envy/next";
import { envSchema } from "./src/env/schema";
syncNextEnv(envSchema, {
clientFile: "src/env/client.ts",
serverFile: "src/env/server.ts",
schemaImportPath: "./schema",
});Run this when public schema keys change.
Use server env only from server code:
import { env } from "@/env/server";Use client env only from client-safe code:
import { env } from "@/env/client";Lint integration forbids raw process.env outside the env files.