npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oviirup/env

v1.0.3

Published

Validate environment variables with zod in Next.JS

Readme

@oviirup/env

npm license ci

Type-safe environment variable validation for JavaScript and TypeScript, built on Zod.

Validate process.env (or any env object) at runtime, infer types from your schemas, and block accidental access to server-only variables on the client.

This package is derived t3-env, with a few minor tweaks and a Zod-only API (t3-env accepts any Standard Schema validator). See env.t3.gg for the original project and full documentation of the upstream API.

Requirements

  • Zod 4 (zod ^4) as a peer dependency
  • ESM ("type": "module")

Installation

bun i @oviirup/env zod
npm i @oviirup/env zod
pnpm add @oviirup/env zod

Quick start

Next.js

Use @oviirup/env/next. It locks prefix to NEXT_PUBLIC_ and strict to true.

// env.ts
import { createEnv } from "@oviirup/env/next";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    SECRET_KEY: z.string().min(32),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
    NEXT_PUBLIC_APP_NAME: z.string().default("My App"),
  },
  vars: {
    DATABASE_URL: process.env.DATABASE_URL,
    SECRET_KEY: process.env.SECRET_KEY,
    NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
    NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
  },
});

The Next.js helper:

  • Sets strict: true so vars must include every schema key
  • Sets prefix: "NEXT_PUBLIC_" so client keys must use that prefix and server keys must not
  • Defaults missing server / client objects to {}

Other frameworks

Use the core entry @oviirup/env when you need a custom prefix or loose vars.

// env.ts
import { createEnv } from "@oviirup/env";
import { z } from "zod";

export const env = createEnv({
  strict: true,
  prefix: "VITE_",
  server: {
    DATABASE_URL: z.string().url(),
    API_SECRET: z.string(),
  },
  client: {
    VITE_API_URL: z.string().url(),
    VITE_APP_ENV: z.enum(["development", "production"]),
  },
  vars: import.meta.env,
});

Usage

Import the validated object anywhere in the app. Types come from your Zod schemas.

// server.ts
import { env } from "./env";

const DB_URL = env.DATABASE_URL; // typed and validated
const SECRET = env.SECRET_KEY; // available on the server

// client.tsx
import { env } from "./env";

const API_URL = env.NEXT_PUBLIC_API_URL; // safe on the client
const DB_URL = env.DATABASE_URL; // throws: server-only access

On the client, only client and shared keys are readable. Accessing any other key calls onBreach (or throws).

API

createEnv(options)

Parses the runtime env against your Zod schemas and returns a readonly object inferred from server, client, shared, and extends.

Entry points:

| Import | Role | | ---------------------- | --------------------------------------------------------------- | | @oviirup/env | Core helper. You set prefix and strict. | | @oviirup/env/next | Next.js helper. prefix is NEXT_PUBLIC_, strict is true. | | @oviirup/env/presets | Ready-made env objects for extends. |

Options

| Option | Default | Description | | ------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------- | | vars | - (runtime fallback: process.env) | Values to validate. Required in the type definitions. | | server | - | Server-only schemas. Keys must not start with prefix. | | client | - | Client schemas. Keys must start with prefix when prefix is set. | | shared | - | Available on both client and server. No prefix required. | | prefix | undefined | Client key prefix. Enforced at the type level. | | strict | false | When true, vars may only contain keys declared in your schemas. | | isServer | isServer() | Override server/client detection. Deno is treated as server. | | skip | false | Skip Zod validation and return the raw vars object. | | onError | throw | Called when validation fails. | | onBreach | throw | Called when a server-only key is read on the client. | | extends | - | Objects merged into the result (presets or computed values). Schema data wins over presets. | | allowEmptyString | false | When false, empty strings are treated as missing (undefined). |

Provide at least one of server or client (or both). prefix is required whenever client is set.

vars

Source object for validation. Pass process.env, import.meta.env, or an explicit map. With strict: true, TypeScript requires every schema key and rejects extras.

vars: process.env;
vars: import.meta.env;
vars: {
  DATABASE_URL: process.env.DATABASE_URL;
}

strict

  • false (default, core): extra keys on vars are allowed
  • true (Next.js helper): vars must match schema keys exactly

prefix

When set:

  • Every client key must start with the prefix
  • No server key may start with the prefix
  • shared keys are exempt
prefix: "NEXT_PUBLIC_";
prefix: "VITE_";

isServer

Default detection treats Node and Deno as server. Override for tests or non-browser runtimes:

isServer: true;
isServer: false;

allowEmptyString

Empty strings in env files are common (SECRET=). By default they are treated as missing (undefined), so optional schemas still pass and required string schemas fail as unset.

Set this to true to keep "" as a string value:

export const env = createEnv({
  server: {
    OPTIONAL_TOKEN: z.string().optional(),
    FLAG: z.string(),
  },
  vars: process.env,
  allowEmptyString: true,
});

With allowEmptyString: true, "" is validated as a string. That can fail z.string().min(1), URL schemas, and similar constraints, but it will satisfy z.string().

skip

Skip validation and return the raw env object. Useful in tests or generated builds where values are not available yet.

skip: process.env.SKIP_ENV_VALIDATION === "true";

onError

Default: throw Invalid environment variables: <paths>.

onError: (error) => {
  const issues = error.issues
    .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
    .join("\n");
  throw new Error(`Environment validation failed:\n${issues}`);
};

onBreach

Default: throw Attempted to access a server-side environment variable on the client: <name>.

onBreach: (variable) => {
  throw new Error(
    `Cannot access server-only variable "${variable}" on the client.`,
  );
};

extends

Merge preset (or other) objects into the result. Later parsed schema values overwrite earlier preset keys.

import { vercel } from "@oviirup/env/presets";
import { createEnv } from "@oviirup/env/next";
import { z } from "zod";

export const env = createEnv({
  extends: [vercel()],
  server: {
    CUSTOM_SERVER_VAR: z.string(),
  },
  client: {
    NEXT_PUBLIC_CUSTOM_CLIENT_VAR: z.string(),
  },
  vars: process.env,
});

Presets

Import from @oviirup/env/presets. Each function returns a validated env object suitable for extends.

| Preset | Purpose | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | vercel() | Vercel system environment variables | | neonVercel() | Neon on Vercel (DATABASE_URL, POSTGRES_*, …) | | supabaseVercel() | Supabase on Vercel (server + NEXT_PUBLIC_* client keys) | | upstashRedis() | Upstash Redis (UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN) | | vite() | Vite (BASE_URL, MODE, DEV, PROD, SSR from import.meta.env) |

import { supabaseVercel, vercel } from "@oviirup/env/presets";
import { createEnv } from "@oviirup/env/next";

export const env = createEnv({
  extends: [vercel(), supabaseVercel()],
  server: {},
  client: {},
  vars: process.env,
});

Type inference

Output types follow Zod output types, including transform and coerce:

import { createEnv } from "@oviirup/env/next";
import { z } from "zod";

export const env = createEnv({
  server: {
    PORT: z.coerce.number(),
    DATABASE_URL: z.string().url(),
  },
  client: {
    NEXT_PUBLIC_API_URL: z.string().url(),
  },
  vars: process.env,
});

// env.PORT: number
// env.DATABASE_URL: string
// env.NEXT_PUBLIC_API_URL: string