GitHub

Next.js

The Next.js package comes preconfigured for Next.js and also enforces some extra rules by default to make sure you have out-of-the-box compatibility in all different Next.js runtimes.

Install dependencies

Install the required dependencies:

pnpm add @t3-oss/env-nextjs zod
pnpm add @t3-oss/env-nextjs zod
💡

@t3-oss/env-nextjs requires a minimum of typescript@4.7.2.

💡

@t3-oss/env-nextjs 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

src/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    OPEN_AI_API_KEY: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
  },
  // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY,
    NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
  },
  // For Next.js >= 13.4.4, you only need to destructure client variables:
  // experimental__runtimeEnv: {
  //   NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
  // }
});
src/env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    OPEN_AI_API_KEY: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
  },
  // If you're using Next.js < 13.4.4, you'll need to specify the runtimeEnv manually
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY,
    NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
  },
  // For Next.js >= 13.4.4, you only need to destructure client variables:
  // experimental__runtimeEnv: {
  //   NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
  // }
});

Unlike in the core package, runtimeEnv is strict by default, meaning you'll have to destructure all the keys manually. This is due to how Next.js bundles environment variables and only explicitly accessed variables are included in the bundle. Missing keys will result in a type-error:

missing runtimeEnv

⚠️

While defining both the 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 two files.

src/env/server.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    OPEN_AI_API_KEY: z.string().min(1),
  },
  runtimeEnv: process.env,
});
src/env/server.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    OPEN_AI_API_KEY: z.string().min(1),
  },
  runtimeEnv: process.env,
});
src/env/client.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  client: {
    PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
  },
  runtimeEnv: process.env,
});
src/env/client.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
 
export const env = createEnv({
  client: {
    PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
  },
  runtimeEnv: process.env,
});

Validate schema on build (recommended)

We recommend you importing your newly created file in your next.config.js. This will make sure your environment variables are validated at build time which will save you a lot of time and headaches down the road. You can use unjs/jiti to import TypeScript files in your next.config.js:

next.config.js
import { fileURLToPath } from "node:url";
import createJiti from "jiti";
const jiti = createJiti(fileURLToPath(import.meta.url));
 
// Import env here to validate during build. Using jiti we can import .ts files :)
jiti("./app/env");
 
/** @type {import('next').NextConfig} */
export default {
  /** ... */
};
next.config.js
import { fileURLToPath } from "node:url";
import createJiti from "jiti";
const jiti = createJiti(fileURLToPath(import.meta.url));
 
// Import env here to validate during build. Using jiti we can import .ts files :)
jiti("./app/env");
 
/** @type {import('next').NextConfig} */
export default {
  /** ... */
};

Use your schema

Then, import the env object in your application and use it, taking advantage of type-safety and auto-completion:

some-api-endpoint.ts
import { env } from "~/env"; // On server
 
export const GET = async () => {
  // do fancy ai stuff
  const magic = await fetch("...", {
    headers: { Authorization: env.OPEN_AI_API_KEY },
  });
  // ...
};
some-api-endpoint.ts
import { env } from "~/env"; // On server
 
export const GET = async () => {
  // do fancy ai stuff
  const magic = await fetch("...", {
    headers: { Authorization: env.OPEN_AI_API_KEY },
  });
  // ...
};
some-component.tsx
import { env } from "~/env"; // On client - same import!
 
export const SomeComponent = () => {
  return (
    <SomeProvider publishableKey={env.PUBLIC_PUBLISHABLE_KEY}>
      {/* ... */}
    </SomeProvider>
  );
};
some-component.tsx
import { env } from "~/env"; // On client - same import!
 
export const SomeComponent = () => {
  return (
    <SomeProvider publishableKey={env.PUBLIC_PUBLISHABLE_KEY}>
      {/* ... */}
    </SomeProvider>
  );
};