Membangun aplikasi yang menghormati privasi pengguna: memahami prinsip GDPR, menetapkan cookie consent yang benar, enkripsi data pribadi, serta memenuhi hak pengguna atas data (akses, hapus, ekspor).

Data pribadi pengguna bukan sekadar baris di database — ia milik pengguna, dipinjamkan ke aplikasi kalian. Episode ini membahas privacy & compliance: bagaimana membangun fitur yang menghormati kepemilikan itu, dengan GDPR sebagai standar dunia yang paling dikenal dan paling banyak menetapkan aturan.
Ini bukan topik "hukum" yang abstrak — hampir semuanya adalah keputusan teknik yang konkret: cookie apa yang dipasang sebelum consent, bagaimana mengenkripsi data, dan bagaimana menjalankan hak hapus data. Mari bedah satu per satu.
GDPR menetapkan prinsip yang punya padanan langsung di implementasi:
| Prinsip GDPR | Arti | Padanan Teknis |
|---|---|---|
| Lawful basis | Setiap pemrosesan punya dasar | Consent dialog yang jelas |
| Data minimization | Kumpulkan hanya yang perlu | Skema tanpa kolom berlebih |
| Storage limitation | Simpan tidak lebih lama dari perlu | Retensi & hapus terjadwal |
| Privacy by design | Privasi dari awal, bukan tambahan | Enkripsi sejak skema |
Kunci yang sering keliru dipahami: GDPR mengatur "legal grounds" — consent hanyalah salah satunya. Yang harus kalian lakukan: tandai dengan jelas di sistem kalian, data mana yang diproses dengan dasar apa.
Banner consent yang "kamu klik untuk membuangnya" adalah pelanggaran paling umum. Pola yang benar:
"use client"
export function CookieConsent() {
const [decision, setDecision] = useState(null)
if (decision) {
document.cookie = `consent=${JSON.stringify(decision)}; max-age=${60*60*24*365}`
}
if (!decision) {
return (
<div className="fixed bottom-4 inset-x-4 bg-white border p-4 rounded-lg shadow">
<p className="text-sm">Kami memakai cookie untuk mengingat sesi login
(esensial) dan, jika kalian setuju, untuk analitik.</p>
<div className="mt-3 flex gap-2">
<button onClick={() => setDecision({ analytics: false })}>
Hanya esensial
</button>
<button onClick={() => setDecision({ analytics: true })}>
Setujui analitik
</button>
</div>
</div>
)
}
}Aturan emasnya: jangan muat Google Analytics (atau script apa pun yang tracking) sebelum consent "Setujui". Kalau kalian memuatnya lebih dulu, kalian sudah melanggar.
Warning
Ingat, cookie consent ini berlaku untuk tracking cookie. Cookie esensial (session, keamanan) sah tanpa consent — bedanya: ia diperlukan untuk fungsi inti. Saat ragu, tanya diri: "apakah fungsi inti aplikasi rusak tanpa cookie ini?" Jika ya, ia esensial.
Dua jenis enkripsi yang wajib dibedakan:
| Jenis | Contoh | Tujuan |
|---|---|---|
| In transit | HTTPS/TLS | Melindungi saat berpindah jaringan |
| At rest | Enkripsi disk | Melindungi file database itu sendiri |
Untuk kolom sangat sensitif (token, data identitas), tambahkan field-level encryption — data terenkripsi di database sehingga membacanya mentah pun tidak berguna:
import crypto from "node:crypto"
const KEY = Buffer.from(process.env.FIELD_KEY, "base64")
export function encryptField(value: string) {
const iv = crypto.randomBytes(12)
const cipher = crypto.createCipheriv("aes-256-gcm", KEY, iv)
const encrypted = Buffer.concat([cipher.update(value), cipher.final()])
return `${iv.toString("base64")}:${encrypted.toString("base64")}`
}Prisma sendiri sudah menangani hashing password dengan aman (episode 6). Enkripsi field ini khusus untuk data yang perlu dibaca ulang oleh aplikasi tapi sebaiknya tidak terbaca mata telanjang di DB.
GDPR memberi pengguna hak yang harus dijalankan oleh sistem, bukan hanya dibalas email manual:
| Hak | Arti | Fitur Teknis |
|---|---|---|
| Access | "Data apa yang kalian simpan tentang saya?" | Endpoint/portal menampilkan data pribadi |
| Erasure | "Hapus data saya" | Endpoint hapus lengkap + semua turunan |
| Portability | "Berikan data saya dalam format standar" | Ekspor JSON/CSV |
| Rectification | "Perbaiki data yang salah" | Profil editable |
Contoh endpoint hapus akun yang benar — menghapus data, bukan menandai akun:
export async function DELETE() {
const session = await auth()
await prisma.$transaction([
prisma.note.deleteMany({ where: { authorId: session.user.id } }),
prisma.session.deleteMany({ where: { userId: session.user.id } }),
prisma.user.delete({ where: { id: session.user.id } }),
])
return Response.json({ ok: true })
}Dan ekspor data dalam format standar — sesuatu yang harus ada untuk compliance:
export async function GET() {
const session = await auth()
const [user, notes] = await Promise.all([
prisma.user.findUnique({ where: { id: session.user.id } }),
prisma.note.findMany({ where: { authorId: session.user.id } }),
])
return Response.json({ user, notes }, {
headers: { "Content-Disposition": 'attachment; filename="data.json"' },
})
}Tip
Hak "hapus data" terdengar sederhana tapi menggali masalah arsitektur: siapa pemilik data? Catatan di database relational saling merujuk (user → notes → comments). Hapus akun berarti hapus semua turunan — gunakan $transaction (seperti contoh) agar hapusnya atomik: semua atau tidak sama sekali. Data yang tersisa setelah "akun dihapus" adalah pelanggaran yang paling sering diaudit.
"Data minimization" punya padanan operasional: retensi. Simpan data pribadi selama masih berguna, lalu hapus. Contoh job terjadwal (episode 13) untuk menghapus data yang sudah basi:
import { logger } from "@/lib/logger"
export async function cleanupExpiredSessions() {
const cutoff = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30) // 30 hari
const { count } = await prisma.session.deleteMany({
where: { expiresAt: { lt: cutoff } },
})
logger.info({ count }, "sesi kedaluwarsa dibersihkan")
}Selesaikan dengan urutan berikut:
CookieConsent dan pastikan analytics tidak dimuat sebelum "Setujui".curl -s http://localhost:3000/api/export -H "Cookie: session=..."Inti yang harus dibawa pulang:
Di episode 21 selanjutnya kita memasuki era AI-fullstack — membangun LLM & RAG apps dengan streaming respons, embedding, dan vector database. Sampai jumpa di episode 21!