Skip to content

Environment Variables and Configuration

Managing environment variables in our project is an essential task, but it can also be challenging. This project uses environment variables for configuration management, leveraging t3-env for type-safe environment variables with runtime validation. When you add new environment variables, you need to update the .env.example file and the src/env.js file to ensure everything is working correctly.

Configuration Files

1. Environment Schema (src/env.js)

The env.js file defines the schema and validation rules for all environment variables using Zod. It’s divided into three main sections:

src/env.js
import { z } from "zod";
import { createEnv } from "@t3-oss/env-nextjs";
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars.
*/
server: {
DATABASE_URL: z
.string()
.url()
.refine(
(str) => !str.includes("YOUR_MYSQL_URL_HERE"),
"You forgot to change the default URL",
),
NEXTAUTH_SECRET:
process.env.NODE_ENV === "production"
? z.string()
: z.string().optional(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
process.env.VERCEL ? z.string() : z.string().url(),
),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
NODE_ENV: z
.enum(["development", "test", "production"])
.default("development"),
BASE_URL: z.string().url(),
BASE_PATH: z.string().optional(),
BOT_API_URL: z.string().url().optional(),
BOT_API_TOKEN: z.string().optional(),
},
/**
* Specify your client-side environment variables schema here. This way you can ensure the app
* isn't built with invalid env vars. To expose them to the client, prefix them with
* `NEXT_PUBLIC_`.
*/
client: {
// NEXT_PUBLIC_CLIENTVAR: z.string(),
},
/**
* You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
* middlewares) or client-side so we need to destruct manually.
*/
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NODE_ENV: process.env.NODE_ENV,
BASE_URL: process.env.NEXT_PUBLIC_BASE_URL
? process.env.NEXT_PUBLIC_BASE_URL
: process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: "https://himarpl.com",
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
BASE_PATH: process.env.NEXT_PUBLIC_BASE_PATH || "",
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
BOT_API_URL: process.env.BOT_API_URL,
BOT_API_TOKEN: process.env.BOT_API_TOKEN,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
* useful for Docker builds.
*/
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
/**
* Makes it so that empty strings are treated as undefined.
* `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error.
*/
emptyStringAsUndefined: true,
});

2. Environment File (.env)

Create a .env file in the project root with your environment-specific values:

Terminal window
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_BASE_PATH=""
NEXT_BASE_URL="http://localhost:3000"
# Database Configuration
DATABASE_URL=
# NextAuth Configuration
NEXTAUTH_SECRET=
NEXTAUTH_URL="http://localhost:3000"
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Upstash Redis Configuration
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# PostHog Configuration (Optional)
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
# Bot Configuration (Optional)
BOT_API_URL=
BOT_API_TOKEN=

Key Features

  • Type Safety: All environment variables are validated at build and runtime
  • Runtime Validation: Ensures your app isn’t built with invalid env vars
  • Client-Side Safety: Clear separation between server and client variables
  • Default Values: Fallbacks for certain variables when not specified
  • Empty String Handling: Empty strings are treated as undefined by default

Usage Guidelines

  1. Server-Side Variables:

    • Access using env.VARIABLE_NAME
    • Never exposed to the client
    • Example: env.DATABASE_URL
  2. Client-Side Variables:

    • Must be prefixed with NEXT_PUBLIC_
    • Can be accessed in both server and client code
    • Example: env.NEXT_PUBLIC_POSTHOG_KEY
  3. Development:

    • Create a local .env file based on .env.example
    • Never commit .env to version control
    • Update env.js schema when adding new variables

Example Usage

src/server/db.ts
import { env } from "@/env";
import { PrismaClient } from "@prisma/client";
const createPrismaClient = () =>
new PrismaClient({
log:
env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
const globalForPrisma = globalThis as unknown as {
prisma: ReturnType<typeof createPrismaClient> | undefined;
};
export const db = globalForPrisma.prisma ?? createPrismaClient();
if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;

You can access the environment variables in your code like this:

import { env } from "@/env.js";
// Server-side usage
const dbUrl = env.DATABASE_URL;
// Client-side usage
const posthogKey = env.NEXT_PUBLIC_POSTHOG_KEY;