Di aplikasi modern, secret seperti API key, token akses, kredensial database, dan signing key sering menjadi titik lemah yang paling berbahaya. Masalahnya jarang ada pada format variabel itu sendiri, tetapi pada di mana secret dibaca, kapan ia diekspos, dan bagaimana aliran datanya menuju browser. Dalam konteks Next.js 16, pengelolaan environment variable harus dipahami dari sudut pandang arsitektur: mana kode yang berjalan di server, mana yang dibundel ke client, dan bagaimana batas antar-keduanya dijaga.

Artikel ini berfokus pada praktik yang aman dan realistis untuk menyimpan serta menggunakan secret pada sisi server di Next.js. Kita juga akan membahas variabel yang memang boleh terekspos ke client, implikasi prefix publik, contoh integrasi API pihak ketiga yang benar melalui server, serta audit sederhana untuk memastikan secret tidak bocor ke bundle, log, atau route handler yang salah desain.

Memahami batas server dan client di Next.js

Next.js memisahkan eksekusi kode ke beberapa konteks, dan pemahaman ini sangat penting sebelum membahas secret:

  • Server: kode berjalan di Node.js atau runtime server lain yang didukung. Di sinilah secret aman untuk dibaca, selama tidak dikirim balik ke browser.
  • Client: kode dibundel dan dikirim ke browser. Apa pun yang sampai di sini harus dianggap publik.
  • Route handler / server action / data fetching server-side: area yang umum dipakai untuk membaca secret dan memanggil layanan pihak ketiga.

Aturan dasarnya sederhana: jika browser bisa mengakses nilainya, itu bukan secret lagi. Karena itu, tidak cukup hanya menaruh API key di file .env. Anda juga harus memastikan key tersebut hanya dipakai di kode yang benar-benar berjalan di server.

Variabel server-only

Variabel tanpa prefix publik seharusnya diperlakukan sebagai server-only. Contohnya:

PAYMENT_API_KEY=sk_live_xxx
DATABASE_URL=postgres://user:pass@host:5432/db
JWT_SIGNING_SECRET=super-secret-value

Variabel seperti ini aman jika dibaca dari kode server dan tidak diserialisasi ke respons HTTP, props client, atau log yang dapat diakses pihak lain.

Variabel publik untuk client

Di Next.js, variabel dengan prefix publik seperti NEXT_PUBLIC_ akan tersedia untuk kode client. Artinya nilainya bisa ikut masuk ke bundle browser atau tersedia saat runtime di sisi client. Contohnya:

NEXT_PUBLIC_APP_ENV=production
NEXT_PUBLIC_ANALYTICS_ID=G-XXXXXXX

Prefix ini bukan fitur keamanan, melainkan mekanisme deklaratif bahwa nilai tersebut memang boleh dipublikasikan. Kesalahan umum adalah menaruh secret pada variabel yang diawali NEXT_PUBLIC_ karena ingin memudahkan akses dari komponen React di browser. Jika dilakukan, secret itu praktis sudah bocor.

Mengelola file .env dengan benar

Secara operasional, file .env hanya sarana memuat konfigurasi. Keamanannya tidak otomatis terjamin. Beberapa praktik yang sebaiknya diterapkan:

  • Gunakan .env.local untuk pengembangan lokal dan jangan commit file yang berisi secret.
  • Simpan nilai produksi di sistem manajemen secret milik platform deploy, CI/CD, atau secret manager khusus.
  • Commit hanya file contoh seperti .env.example tanpa nilai sensitif.
  • Validasi keberadaan variabel saat startup agar kegagalan terjadi lebih awal dan jelas.

Contoh validasi sederhana:

const requiredEnv = [
  'PAYMENT_API_KEY',
  'DATABASE_URL',
  'JWT_SIGNING_SECRET'
] as const

for (const key of requiredEnv) {
  if (!process.env[key]) {
    throw new Error(`Missing required environment variable: ${key}`)
  }
}

Validasi seperti ini berguna untuk mencegah situasi di mana aplikasi berjalan dengan konfigurasi setengah lengkap, lalu pengembang menambahkan fallback yang justru tidak aman.

Kesalahan umum saat membaca environment variable

  • Membaca secret di file client component. Jika file memiliki kebutuhan client-side, jangan akses secret di sana.
  • Meneruskan secret sebagai props dari server component ke client component.
  • Mengembalikan objek konfigurasi mentah dari route handler yang memuat env sensitif.
  • Menggunakan secret sebagai bagian dari pesan error atau debug log.

Pola aman: integrasi API pihak ketiga dari server, bukan browser

Salah satu kebutuhan paling umum adalah memanggil API pihak ketiga, misalnya payment gateway, email provider, atau layanan AI. Jika API tersebut memerlukan secret key, panggilan harus dilakukan dari server. Browser hanya berkomunikasi dengan endpoint aplikasi Anda sendiri.

Berikut contoh route handler yang memanggil API pihak ketiga dari server:

import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
  const apiKey = process.env.PAYMENT_API_KEY

  if (!apiKey) {
    return NextResponse.json(
      { error: 'Server misconfiguration' },
      { status: 500 }
    )
  }

  const body = await req.json()

  const upstreamRes = await fetch('https://api.example-payments.com/v1/charge', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`
    },
    body: JSON.stringify({
      amount: body.amount,
      currency: 'IDR',
      customerId: body.customerId
    })
  })

  if (!upstreamRes.ok) {
    const text = await upstreamRes.text()
    return NextResponse.json(
      { error: 'Failed to create charge', detail: text },
      { status: 502 }
    )
  }

  const result = await upstreamRes.json()

  return NextResponse.json({
    chargeId: result.id,
    status: result.status
  })
}

Mengapa pola ini benar?

  • Secret dibaca hanya di server.
  • Browser tidak pernah melihat API key pihak ketiga.
  • Aplikasi Anda dapat memfilter input dan output, sehingga data yang dikirim ke client hanya yang memang diperlukan.
  • Anda bisa menambahkan otorisasi, rate limiting, audit log, dan validasi sebelum memanggil upstream API.

Pola yang salah

Berikut pola yang harus dihindari:

'use client'

export async function createCharge(amount: number) {
  await fetch('https://api.example-payments.com/v1/charge', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.NEXT_PUBLIC_PAYMENT_API_KEY}`
    },
    body: JSON.stringify({ amount })
  })
}

Di sini, secret telah menjadi publik karena ditempatkan di variabel dengan prefix NEXT_PUBLIC_ dan dipakai langsung dari browser. Selain bocor, API pihak ketiga juga menjadi rentan disalahgunakan karena pengguna dapat menyalin key dari DevTools atau traffic jaringan.

Mencegah kebocoran secret ke bundle client

Kebocoran secret tidak selalu terjadi karena niat eksplisit. Sering kali kebocoran muncul karena desain modul yang kurang rapi. Contoh umum:

  • Satu file utilitas berisi helper server dan client sekaligus, lalu tanpa sengaja mengimpor process.env.SECRET_KEY.
  • Konfigurasi global diekspor penuh, lalu digunakan baik di server maupun client.
  • Objek hasil baca env dikirim ke komponen client untuk keperluan debug.

Pisahkan konfigurasi menjadi dua lapisan:

  • Server config: hanya untuk kode server, berisi secret dan konfigurasi privat.
  • Public config: hanya berisi nilai yang memang aman untuk browser.

Contoh:

// lib/config/server.ts
export const serverConfig = {
  paymentApiKey: process.env.PAYMENT_API_KEY!,
  databaseUrl: process.env.DATABASE_URL!
}

// lib/config/public.ts
export const publicConfig = {
  appEnv: process.env.NEXT_PUBLIC_APP_ENV ?? 'development',
  analyticsId: process.env.NEXT_PUBLIC_ANALYTICS_ID ?? ''
}

Dengan pemisahan ini, peluang file client mengimpor secret menjadi lebih kecil. Secara praktik, jangan pernah mengimpor server.ts dari komponen client atau modul yang dipakai browser.

Audit sederhana terhadap bundle client

Setelah build, lakukan audit dasar:

  1. Cari nama variabel sensitif di kode sumber: PAYMENT_API_KEY, DATABASE_URL, SECRET, dan variasinya.
  2. Cari apakah ada modul server config yang diimpor oleh file bertanda 'use client'.
  3. Periksa hasil build statis atau artefak yang dikirim ke browser jika pipeline Anda memungkinkan inspeksi bundle.
  4. Uji aplikasi melalui browser DevTools dan lihat apakah ada request langsung ke API pihak ketiga yang seharusnya hanya dipanggil dari server.

Jika Anda melihat domain upstream sensitif dipanggil langsung dari browser, hampir pasti ada desain yang salah.

Mencegah kebocoran ke log dan error response

Secret juga sering bocor melalui jalur yang tidak disadari: log server, monitoring, dan pesan error. Misalnya, pengembang mencetak seluruh objek request header, seluruh konfigurasi aplikasi, atau respons upstream mentah yang ternyata mengandung token.

Beberapa aturan praktis:

  • Jangan pernah melakukan console.log(process.env).
  • Jangan log header Authorization, cookie, token, atau kredensial.
  • Redaksi error internal sebelum dikirim ke client.
  • Gunakan structured logging dan filter field sensitif jika tersedia.

Contoh yang lebih aman:

try {
  // call upstream
} catch (error) {
  console.error('Payment upstream failed', {
    message: error instanceof Error ? error.message : 'unknown error'
  })

  return Response.json(
    { error: 'Upstream service error' },
    { status: 502 }
  )
}

Pendekatan ini menjaga agar informasi yang berguna untuk debugging tetap ada tanpa membocorkan kredensial atau payload sensitif.

Desain route handler yang aman

Route handler sering menjadi lokasi utama penggunaan secret, sehingga desainnya perlu disiplin. Berikut prinsip yang penting:

  • Validasi input: jangan teruskan input browser mentah ke API pihak ketiga.
  • Minimalkan output: kirim hanya field yang diperlukan client.
  • Tambahkan otorisasi: jangan jadikan route sebagai proxy terbuka ke layanan upstream.
  • Batasi metode dan skenario: hanya izinkan operasi yang memang dibutuhkan aplikasi.

Kesalahan desain yang sering terjadi adalah membuat endpoint generik seperti /api/proxy yang menerima URL mana pun, header mana pun, lalu menambahkan secret server ke request tersebut. Ini berbahaya karena bisa berubah menjadi open proxy atau kanal penyalahgunaan terhadap akun layanan pihak ketiga Anda.

Kapan variabel publik memang diperlukan?

Tidak semua variabel harus dirahasiakan. Beberapa nilai memang pantas dipublikasikan, misalnya:

  • ID analitik publik
  • Nama environment untuk tampilan UI
  • Base URL publik yang tidak sensitif
  • Feature flag yang memang aman diketahui client

Namun tetap berhati-hati. Informasi yang terlihat tidak sensitif bisa membantu penyerang memetakan sistem Anda. Jadi, meskipun boleh publik, tetap pertimbangkan apakah variabel tersebut benar-benar diperlukan di browser.

Checklist audit praktis

Berikut audit sederhana yang dapat diterapkan di proyek Next.js 16:

  1. Pastikan semua secret tidak menggunakan prefix NEXT_PUBLIC_.
  2. Tinjau semua file 'use client' dan pastikan tidak ada akses ke secret atau impor modul config server.
  3. Pastikan pemanggilan API pihak ketiga yang memerlukan key dilakukan melalui route handler, server action, atau kode server lain.
  4. Periksa respons API internal Anda: apakah ada field debug, header, atau payload upstream yang seharusnya disembunyikan?
  5. Audit logging: hapus log yang mencetak env, header sensitif, token, cookie, atau konfigurasi penuh.
  6. Gunakan .env.example tanpa nilai rahasia, dan simpan secret produksi di platform secret manager atau dashboard deploy.
  7. Lakukan pengujian manual di browser DevTools untuk memastikan tidak ada request langsung dari client ke layanan sensitif.

Penutup

Mengamankan secret di Next.js 16 bukan sekadar menyimpan nilai di file .env. Intinya adalah memastikan secret hanya dibaca di server, tidak dibundel ke client, tidak tampil di log, dan tidak keluar melalui route handler yang terlalu permisif. Prefix publik seperti NEXT_PUBLIC_ harus dipahami sebagai deklarasi bahwa nilai tersebut memang boleh dilihat browser, bukan cara menyimpan rahasia.

Jika Anda mengintegrasikan API pihak ketiga, gunakan server sebagai perantara yang memegang secret, memvalidasi input, dan menyaring output. Tambahkan audit sederhana pada impor modul, hasil build, log, dan desain endpoint. Dengan disiplin ini, Anda tidak hanya mengurangi risiko kebocoran secret, tetapi juga membangun batas arsitektur yang lebih sehat antara server dan client.