~/TechPurAI
~/tutorials/nextjs-from-scratch/environment-variables
intermediate·part 16 of 22·2 min read

Environment variables and secrets

Updated Aug 16, 2026JavaScript · Next.js

submitComment from part 15 logs a comment to the console — a real implementation saves it to a database, which means a connection string that has no business being hardcoded in lib/posts.ts or committed to git. This part covers where that actually goes.

.env.local

bash
# .env.local
DATABASE_URL="postgres://user:password@localhost:5432/devnotes"
bash
# .gitignore — already includes this by default in create-next-app
.env*.local

Next.js loads .env.local automatically — no package to install, no manual dotenv import. create-next-app already gitignores it by default specifically so a real secret never gets committed by accident.

Reading it — server-side only, by default

ts
// lib/db.ts
const connectionString = process.env.DATABASE_URL;

process.env.DATABASE_URL here works only in code that runs on the server — inside a Server Component, a Server Action, or a Route Handler, matching exactly the server-only code covered in part 3. This is the safe default: nothing about it is ever sent to the browser.

The NEXT_PUBLIC_ prefix: a real boundary, not a formality

bash
# .env.local
NEXT_PUBLIC_SITE_URL="https://devnotes.example.com"
DATABASE_URL="postgres://user:password@localhost:5432/devnotes"
tsx
// a Client Component
"use client";

export function ShareButton() {
  const url = process.env.NEXT_PUBLIC_SITE_URL;
  // ...
}

Only variables prefixed exactly NEXT_PUBLIC_ get inlined into the client-side JavaScript bundle at build time — DATABASE_URL above, with no prefix, is undefined in any Client Component, by design. This isn't a convention to remember to follow; it's how Next.js's build process actually works, and it's the reason a real secret should never be prefixed NEXT_PUBLIC_ even temporarily while testing something — once it's built with that prefix, it's in the shipped JavaScript, readable by anyone who opens devtools.

Different values per environment

text
.env.local           — local dev only, gitignored, real secrets
.env.development      — committed, shared dev defaults
.env.production        — committed, shared production defaults

.env.local always wins over the more general .env.development/.env.production files when present — the standard pattern is committing non-sensitive shared defaults in the latter, while every real secret lives only in .env.local locally and in the hosting platform's own environment variable settings in production (part 22 covers exactly that).

Common mistake

Prefixing a variable NEXT_PUBLIC_ "just to make sure it's available everywhere" without checking whether it's actually meant to be public. NEXT_PUBLIC_SITE_URL above is genuinely fine to expose — it's already visible in every page's URL bar. A database connection string, an API key with write access, a JWT signing secret: never this prefix, regardless of where in the code it's needed.

Next: rendering strategies — this series has used static generation without naming it as such; time to cover SSG, SSR, and ISR as the deliberate choices they actually are.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous15. Building a comment form with a Server Actionnext →17. Rendering strategies: SSG, SSR, and ISR