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
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
# 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
Copy .env.example to .env at the project root:
Fill in required variables
Edit the .env file and fill in all required variables. See .env.example for references and generation instructions.
When adding new environment variables:
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
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