Installation

Install the core package and get started with envin.

The core package can be used in any framework of your choice. To use it, figure out what prefix your framework uses for exposing environment variables to the client. For example, Astro uses PUBLIC_*, while Vite uses VITE_*. You should be able to find this in the framework's documentation.

Install dependencies

First, install the core package:

npm install envin

Then install a schema validator. envin supports any validator that implements the Standard Schema specification:

npm install zod

Although we'll use Zod as examples throughout these docs, you can use any validator that supports Standard Schema, including Valibot, ArkType, and others.

envin requires a minimum of typescript@5.0.0.

envin is an ESM only package. Make sure that your tsconfig uses a module resolution that can read package.json#exports (Bundler is recommended).

Create your schema

Create an environment configuration file (we recommend env.config.ts, but you can name it whatever you want):

Some frameworks generate an env.d.ts file that will collide with env.ts, which means you may need to name it something else like env.config.ts.

env.config.ts
import {  } from "envin";
import * as  from "zod"; 

const env = ({
const env: {
    readonly NODE_ENV: "development" | "production" | "test";
    readonly DATABASE_URL: string;
    readonly OPENAI_API_KEY: string;
    readonly JWT_SECRET: string;
    readonly NEXT_PUBLIC_PUBLISHABLE_KEY: string;
    readonly _schema: FinalSchema<...>;
}
/** * Specify environment variables that can be accessed on both client and server. * You'll get a type error if you try to access a server-side env var on the client. */ : { : .(["development", "production", "test"]) .("development"), }, /** * Specify your server-side environment variables schema here. * This way you can ensure the app isn't built with invalid env vars. */ : { : .(), : .().(1), : .().(32), }, /** * The prefix that client-side variables must have. This is enforced both at * a type-level and at runtime. */ : "NEXT_PUBLIC_", /** * Specify your client-side environment variables schema here. * For them to be exposed to the client, prefix them with your framework's public prefix. */ : { : .().(1), }, /** * What object holds the environment variables at runtime. * This is usually `process.env` or `import.meta.env`. */ : ., }); export default ;

If you only need server-side variables, you can omit the client and clientPrefix properties:

env.config.ts
import {  } from "envin";
import * as  from "zod"; 

export default ({
  : {
    : .(),
    : .().(1),
  },
  : {
    : .(["development", "production", "test"]),
  },
  : .,
});

Server vs client patterns

Each environment variable name in your deployment (for example DATABASE_URL or NEXT_PUBLIC_API_URL) should appear in exactly one of shared, server, or client. Do not declare the same key in more than one block.

  • shared — Use when one name is read on both client and server (typical examples: NODE_ENV, or a flag both bundles need). The value is part of the same contract everywhere.
  • server — Unprefixed names that only server code should read (secrets, database URLs, OAuth client secrets).
  • client — Names that match clientPrefix (for example NEXT_PUBLIC_* in Next.js). Only these are intended for client bundles.

Do I need both GOOGLE_CLIENT_ID and NEXT_PUBLIC_GOOGLE_CLIENT_ID?

Often yes, if your code uses two different names: server routes read GOOGLE_CLIENT_ID, while client code reads NEXT_PUBLIC_GOOGLE_CLIENT_ID (because the framework only exposes prefixed variables to the browser). They are two entries in your schema and, in .env, two assignments. It is normal for them to have the same value (the OAuth client ID string is public; the secret stays in server only).

env.config.ts
import {  } from "envin";
import * as  from "zod";

export default ({
  : "NEXT_PUBLIC_",
  : {
    : .(["development", "production", "test"]),
  },
  : {
    : .().(1),
    : .().(1),
  },
  : {
    : .().(1),
  },
  : .,
});

If you only use the prefixed name everywhere (including server), you can model a single variable under client and omit an unprefixed GOOGLE_CLIENT_ID. Pick one approach per app and keep names consistent with how your framework exposes env vars.

Should the same value appear in shared and client?

No. If a variable is listed under shared, do not list it again under client or server. Use shared only when there is a single env key that both sides read. If the browser must use a different name (prefixed) than the server, use server + client as in the example above, not shared plus duplicates.

While defining both client and server schemas in a single file provides the best developer experience, it also means that your validation schemas for the server variables will be shipped to the client. If you consider the names of your variables sensitive, you should split your schemas into separate files.

env/server.ts
import {  } from "envin";
import * as  from "zod"; 

export const  = ({
  : {
    : .(),
    : .().(1),
  },
  : .,
});
env/client.ts
import {  } from "envin";
import * as  from "zod"; 

export const  = ({
  : "NEXT_PUBLIC_",
  : {
    : .().(1),
  },
  : .,
});

For reusing the same shared (and other) blocks across packages in a monorepo, see Share variables.

For all available options, see Customization.

You'll notice that if your clientPrefix is PUBLIC_, you won't be allowed to enter any other keys in the client object without getting type-errors. Below you can see we get a descriptive error when we set VITE_PUBLIC_API_URL instead of PUBLIC_API_URL:

env.config.ts
import {  } from "envin";
import * as  from "zod"; 

export default ({
  : "PUBLIC_",
  : {
    VITE_PUBLIC_API_URL: .(),
Type 'ZodURL' is not assignable to type '"VITE_PUBLIC_API_URL is not prefixed with PUBLIC_."'.
}, : ., });

The steps required to validate your schema on build will vary from framework to framework, but you'll usually be able to import the env file in your configuration file, or in any file that's pulled in at the beginning of the build process.

For Next.js, you can import your env config in next.config.ts:

next.config.ts
import type { NextConfig } from "next";
import "./env.config"; 

const : NextConfig = {
  // Your Next.js config
};

export default ;

Note that some frameworks don't import their environment variables in their configuration file, so you may need to import your env config in a different location.

Use your environment variables

Import the env object in your application and use it with full type-safety and auto-completion:

app/api/auth/route.ts
import env from "~/env.config";

export async function POST() {
  // ✅ Fully typed and validated
  const response = await fetch("https://api.openai.com/v1/chat/completions", {
    headers: {
      Authorization: `Bearer ${env.OPENAI_API_KEY}`, // string
    },
  });

  // ✅ Access shared variables anywhere
  if (env.NODE_ENV === "development") {
    console.log("Development mode");
  }

  return Response.json({ success: true });
}
components/ClientComponent.tsx
"use client";

import env from "~/env.config";

export function ClientComponent() {
  // ✅ Works on client
  const publishableKey = env.NEXT_PUBLIC_PUBLISHABLE_KEY;

  // ❌ This will throw a runtime error on the client
  // const apiKey = env.OPENAI_API_KEY;

  return <div>Client component</div>;
}

Strict runtime - envStrict

Exactly one of envStrict or env must be provided.

If your framework doesn't bundle all environment variables by default, but instead only bundles the ones you use, you can use the envStrict option to ensure you don't forget to add any variables to your runtime:

env.config.ts
import {  } from "envin";
import * as  from "zod"; 

export default ({
  : {
    : .(),
    : .().(1),
  },
  : {
    : .().(1),
  },
  : "NEXT_PUBLIC_",
  /**
   * Makes sure you explicitly access **all** environment variables
   * from `server` and `client` in your `envStrict`.
   */
  envStrict: {
Property 'DATABASE_URL' is missing in type '{ OPENAI_API_KEY: string | undefined; NEXT_PUBLIC_PUBLISHABLE_KEY: string | undefined; }' but required in type 'Record<"DATABASE_URL" | "OPENAI_API_KEY" | "NEXT_PUBLIC_PUBLISHABLE_KEY", string | number | boolean | undefined>'.
: .., : .., }, });

When using the strict option, missing any of the variables in envStrict will result in a type error.

On this page