Kesalahan keamanan Next.js yang paling umum saya lihat dalam code review: pengembang memberi nama variabel NEXT_PUBLIC_DATABASE_URL dan bertanya-tanya mengapa kata sandi database mereka terlihat di tab Network browser. Prefiks NEXT_PUBLIC_ bukan hanya konvensi penamaan — ini mengontrol apakah variabel diinline ke dalam bundle JavaScript sisi klien pada waktu build dan disajikan kepada setiap pengguna yang mengunduh halaman.
Next.js memiliki dua kategori variabel environment: server-only (tanpa prefiks) dan dapat diakses klien (prefiks NEXT_PUBLIC_). Variabel server-only tersedia di process.env di server — dalam API routes, server components, getServerSideProps, dan server actions. Mereka tidak pernah disertakan dalam bundle klien. Variabel NEXT_PUBLIC_ diinline ke dalam bundle JavaScript pada waktu build — mereka digantikan dengan nilai literal mereka dalam JavaScript yang dikompilasi. Jangan pernah memasukkan kredensial, kunci privat, atau secrets dalam variabel NEXT_PUBLIC_.
ID analitik publik (GA4 measurement ID, Amplitude API key), URL CDN publik, feature flags yang aman untuk diekspos, environment aplikasi (development/staging/production), dan client ID OAuth publik aman sebagai variabel NEXT_PUBLIC_. String koneksi database, JWT secrets, kunci API privat, client secret OAuth, kunci enkripsi, dan secrets penandatanganan webhook harus menjadi variabel server-only.
Next.js memuat file environment dalam urutan prioritas ini (tertinggi dulu): 1) process.env (variabel environment yang sudah diatur dari OS atau platform deployment), 2) .env.local (selalu dimuat, tidak pernah di-commit ke git), 3) .env.{development|test|production} (spesifik lingkungan), 4) .env (file dasar, di-commit ke git untuk nilai non-secret). File .env.local harus selalu ada di .gitignore.
Next.js Environment Variable Security:
────────────────────────────────────────────────────────────
NEXT_PUBLIC_* variables Server-only variables
──────────────────────── ──────────────────────────
Inlined into JS bundle at build Only available in process.env
Visible to ALL users Never sent to browser
✓ GA4 Measurement ID ✓ DATABASE_URL
✓ Public CDN base URL ✓ JWT_SECRET
✓ Feature flags (safe ones) ✓ STRIPE_SECRET_KEY
✗ NEVER database credentials ✓ SENDGRID_API_KEY
✗ NEVER private API keys ✓ WEBHOOK_SIGNING_KEY
File priority (highest → lowest):
process.env (OS/deployment platform) [always wins]
.env.local (personal, never commit) [local override]
.env.production / .env.development [environment-specific]
.env (committed, non-secrets) [base defaults]
Example .env (safe to commit):
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_APP_URL=https://matthewswong.com
NODE_ENV=production
Example .env.local (NEVER commit — .gitignore):
DATABASE_URL=postgresql://user:secret@localhost/mydb
JWT_SECRET=my-local-dev-secret-change-in-prodDari pengalaman saya mengelola variabel environment di seluruh pengembangan, staging, dan produksi: validasi semua variabel environment yang diperlukan saat startup aplikasi, bukan saat runtime saat pertama kali digunakan. Saya menggunakan paket `t3-oss/env-nextjs` (atau skema Zod kustom) untuk memvalidasi dan mengetik variabel environment saat waktu build dan saat aplikasi dimulai. Jika variabel yang diperlukan hilang di produksi, aplikasi gagal segera dengan pesan error yang jelas.
Menyimpan secrets sebagai variabel environment adalah pendekatan yang tepat untuk sebagian besar aplikasi, tetapi dari mana variabel environment itu berasal penting. Untuk deployment Vercel, gunakan variabel environment terenkripsi Vercel — tersimpan terenkripsi, tersedia untuk build dan serverless functions, dan dapat dibatasi lingkupnya per environment. Untuk deployment self-hosted, pertimbangkan Doppler, HashiCorp Vault, atau AWS Secrets Manager.
Next.js Server Actions berjalan di server tetapi dipanggil dari klien melalui permintaan POST ke endpoint server. Tubuh fungsi tidak diekspos ke klien — tetapi berhati-hatilah untuk tidak mengembalikan nilai secret dari Server Action. Jika Server Action mengembalikan `process.env.INTERNAL_API_KEY`, klien menerima nilai itu dalam respons. Terapkan prinsip 'pengungkapan minimum' yang sama ke nilai kembalian Server Action seperti respons API route.
// env.ts — type-safe environment variable validation
import { createEnv } from "@t3-oss/env-nextjs"
import { z } from "zod"
export const env = createEnv({
// Server-only variables (never exposed to browser)
server: {
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, "JWT secret must be 32+ chars"),
SENDGRID_API_KEY: z.string().startsWith("SG."),
NODE_ENV: z.enum(["development", "test", "production"]),
REVALIDATION_SECRET: z.string().min(16),
},
// Client-accessible variables (safe to expose)
client: {
NEXT_PUBLIC_APP_URL: z.string().url(),
NEXT_PUBLIC_GA_ID: z.string().regex(/^G-[A-Z0-9]+$/).optional(),
},
// Map process.env keys to validation schema
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
JWT_SECRET: process.env.JWT_SECRET,
SENDGRID_API_KEY: process.env.SENDGRID_API_KEY,
NODE_ENV: process.env.NODE_ENV,
REVALIDATION_SECRET: process.env.REVALIDATION_SECRET,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_GA_ID: process.env.NEXT_PUBLIC_GA_ID,
},
})
// Usage — TypeScript catches wrong variable access at compile time:
import { env } from "@/env"
// In a server component or API route:
const db = createPool({ connectionString: env.DATABASE_URL }) // ✓ type-safe
// In a client component: TypeScript error if you access server vars!
const url = env.NEXT_PUBLIC_APP_URL // ✓ allowed in client
// const secret = env.JWT_SECRET // ✗ TypeScript ERROR in client code
// .env.example (commit this — documents required vars)
DATABASE_URL=postgresql://user:pass@localhost/mydb
JWT_SECRET=your-32-char-minimum-secret-here
SENDGRID_API_KEY=SG.your-key-here
NEXT_PUBLIC_APP_URL=http://localhost:3000TypeScript mengetik semua variabel `process.env` sebagai `string | undefined`. Dalam kode produksi, Anda akan sering menulis `process.env.DATABASE_URL!` untuk menegaskan variabel didefinisikan — yang gagal secara diam-diam jika variabel benar-benar hilang. Gunakan skema Zod untuk memvalidasi dan mengubah variabel environment saat startup, lalu ekspor objek yang divalidasi sebagai `env`.
Jika Anda secara tidak sengaja meng-commit file .env yang berisi kredensial nyata, menghapusnya di commit berikutnya tidak memperbaiki masalah — secret masih ada di riwayat git. Siapa pun yang meng-clone repositori dapat menemukan file yang dihapus. Perbaikan: rotasi kredensial yang diekspos segera (batalkan yang lama, buat yang baru), kemudian gunakan BFG Repo Cleaner atau `git filter-repo` untuk menulis ulang riwayat.
Untuk proyek dengan pengembangan lokal, staging, dan environment produksi, saya menggunakan: .env (di-commit, default non-secret — URL dasar, feature flags, ID publik), .env.local (tidak di-commit, kredensial pribadi pengembang untuk pengembangan lokal), variabel environment Vercel (terenkripsi, per-environment: development, preview, production).
Jalankan audit berkala dari variabel environment Anda untuk menangkap: variabel NEXT_PUBLIC_ yang berisi data sensitif, variabel yang tidak digunakan yang mengacaukan konfigurasi, variabel yang hilang yang dapat menyebabkan kegagalan runtime, dan variabel duplikat di seluruh environment yang seharusnya berbeda. Saya menjalankan `grep -r 'NEXT_PUBLIC_' src/ --include='*.ts' --include='*.tsx'` untuk menemukan semua penggunaan variabel publik dan memverifikasi setiap satu secara manual.