Skip to main content

Overview

This project uses a centralized, type-safe environment variable system. There’s one .env file to rule them all at the project root, but each package only gets what it asks for through validation.

Environment Stages

The project supports five environment stages in order: local → development → test → staging → production
  • local: Your local development machine.
  • development: Development server/environment.
  • test: Testing environment.
  • staging: Pre-production environment. Production-like settings.
  • production: Production environment.
The NODE_ENV environment variable is centralized in @packages/env and validated across all packages.

How It Works

Single Source of Truth

All environment variables are stored in a single .env file at the project root. The @packages/env package automatically loads this file using dotenv configured in packages/env/src/lib/utils.ts.

Package-Specific Validation

Each package defines only the environment variables it needs:
  • api-hono: HONO_APP_URL, HONO_PORT, HONO_TRUSTED_ORIGINS
  • auth: BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, HONO_APP_URL, HONO_TRUSTED_ORIGINS
  • db: POSTGRES_URL
  • web-next:
    • Server: INTERNAL_API_URL (server-side only, for internal service-to-service calls)
    • Client: NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_API_URL, NEXT_PUBLIC_POSTHOG_HOST, NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_USERJOT_URL (exposed to browser)
Server variables cannot be used on the client. Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser. This prevents sensitive data like secrets and API keys from being exposed in client-side code.

Implementation

Each package uses @t3-oss/env-core with Zod schemas for type-safe validation:
packages/env/src/api-hono.ts
import { createEnv } from "@t3-oss/env-core"
import { z } from "zod"
import "./lib/utils" // Loads root .env
import { NODE_ENV } from "./lib/constants"

export const env = createEnv({
  server: {
    NODE_ENV, // Centralized enum: local, development, test, staging, production
    HONO_APP_URL: z.url(),
    HONO_PORT: z.coerce.number().default(4000),
    HONO_TRUSTED_ORIGINS: z.string().transform(...),
  },
  runtimeEnv: {
    NODE_ENV: process.env.NODE_ENV,
    HONO_APP_URL: process.env.HONO_APP_URL,
    HONO_PORT: process.env.HONO_PORT,
    HONO_TRUSTED_ORIGINS: process.env.HONO_TRUSTED_ORIGINS,
  },
})

Usage in Packages

Import the specific env for your package:
// api/hono/src/index.ts
import { env } from "@packages/env/api-hono"

// web/next/src/lib/config.ts
import { env } from "@packages/env/web-next"

Required Variables

Here are all the environment variables from .env.example:

Server Variables

.env
NODE_ENV=local

# Server variables
HONO_APP_URL=http://localhost:4000
HONO_TRUSTED_ORIGINS=http://localhost:3000
HONO_RATE_LIMIT=60 # allow 60 req/min (default) and twice that for authenticated users
HONO_RATE_LIMIT_WINDOW_MS=60000 # 1 minute (default)

# Generate using `openssl rand -base64 32`
BETTER_AUTH_SECRET=

# Generate at `https://github.com/settings/developers`
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

# Generate at `https://console.cloud.google.com/apis/credentials`
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

# Generate using `bunx pglaunch -k`
POSTGRES_URL=

Client Variables

.env
# Client variables (exposed to browser)
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:4000

# Optional: PostHog Analytics
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
NEXT_PUBLIC_POSTHOG_KEY=

# Optional: User feedback
NEXT_PUBLIC_USERJOT_URL=

Creating and Managing

1
Create .env file
2
Copy .env.example to .env at the project root:
3
cp .env.example .env
4
Fill in required variables
5
Edit the .env file and fill in all required variables. See .env.example for references and generation instructions.
6
Add new variables
7
When adding new environment variables:
8
  • Add to .env file
  • Define in the appropriate package’s env file (packages/env/src/*.ts)
  • Add to server or client schema with Zod validation
  • Add to runtimeEnv object
  • Add to turbo.json: Include the variable in the globalEnv array so Turbo’s cache invalidates correctly when it changes
  • 9
    Validation
    10
    Each package validates only its declared variables at runtime. Invalid values will throw clear error messages.

    Example: Web Next Configuration

    Here’s how the Next.js web app configures its environment variables:
    packages/env/src/web-next.ts
    import { createEnv } from "@t3-oss/env-core"
    import { z } from "zod"
    import "@/lib/utils"
    import { NODE_ENV } from "@/lib/constants"
    
    export const env = createEnv({
      server: {
        NODE_ENV,
        INTERNAL_API_URL: z.url().optional(),
      },
      clientPrefix: "NEXT_PUBLIC_",
      client: {
        NEXT_PUBLIC_APP_URL: z.url(),
        NEXT_PUBLIC_API_URL: z.url(),
        NEXT_PUBLIC_NODE_ENV: NODE_ENV,
        NEXT_PUBLIC_POSTHOG_HOST: z.url().optional(),
        NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
        NEXT_PUBLIC_USERJOT_URL: z.url().optional(),
      },
      runtimeEnv: {
        NODE_ENV: process.env.NODE_ENV,
        INTERNAL_API_URL: process.env.INTERNAL_API_URL,
        NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
        NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
        NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
        NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
        NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
        NEXT_PUBLIC_USERJOT_URL: process.env.NEXT_PUBLIC_USERJOT_URL,
      },
      emptyStringAsUndefined: true,
      skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
    })
    

    Features

    • Type-safe with TypeScript inference
    • Runtime validation with clear error messages
    • Selective access per package
    • Server/client separation (only NEXT_PUBLIC_* exposed to browser)
    • Centralized NODE_ENV enum supporting: local, development, test, staging, production
    • Environment-specific logging (only logs in local environment)
    • Empty strings treated as undefined
    • Optional skip validation for edge cases

    Build docs developers (and LLMs) love