karawaci.kode

2026-05-18 · 5 min

JWT vs Session Cookies di 2026: Pemenang yang Jelas

Lima tahun yang lalu, JWT adalah “yang benar”. Stateless, scalable, microservice-friendly. Saya pakai untuk semua project. Sekarang, untuk semua project baru saya, default-nya session cookies. Inilah kenapa.

Yang JWT advertised, tapi tidak deliver untuk SMB Indonesia

Statelessness sebagai feature. Idealisasi: tidak perlu DB query untuk validate user pada setiap request. Reality: hampir semua app perlu DB lookup untuk:

  • User role & permissions (tidak boleh stale di JWT)
  • Account status (kalau suspended, JWT yang sudah issued masih valid sampai expire)
  • Email/profile changes
  • Plan/tier upgrades

Hasil: tetap query DB per request. JWT statelessness jadi optimization yang sebenarnya tidak save apa-apa.

Multi-service auth. Idealisasi: shared secret, microservice trust JWT. Reality: most Indonesian SMB SaaS punya 1-2 services. Multi-service complexity tidak ada.

Mobile-friendly. Idealisasi: stateless lebih mudah untuk mobile app. Reality: mobile app punya secure storage (Keychain/Keystore) yang lebih cocok untuk session token daripada JWT.

Yang session cookies provide, tanpa drama

Revocation instant. User logout, sysadmin delete session. Token gone. Tidak harus tunggu JWT expire 24 jam.

Roll-out token rotation murah. Update session metadata kapanpun. Tidak ada migrasi key rotation drama.

Same-site cookie protection. Built-in CSRF defense via SameSite=Lax. Tidak butuh CSRF token tambahan untuk most use case.

XSS-resistance via HttpOnly. Cookie tidak bisa di-access dari JavaScript. JWT di localStorage = XSS leak.

My setup sekarang

// session.ts
import { randomBytes } from 'crypto';
import { Database } from 'bun:sqlite';

const db = new Database('sessions.db');
db.exec(`
  CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    created_at INTEGER NOT NULL,
    expires_at INTEGER NOT NULL,
    user_agent TEXT,
    ip TEXT
  );
  CREATE INDEX IF NOT EXISTS idx_expires ON sessions(expires_at);
`);

export function createSession(userId: string, req: Request) {
  const id = randomBytes(32).toString('base64url');
  const now = Date.now();
  const expires = now + (30 * 24 * 60 * 60 * 1000); // 30 days
  
  db.prepare(`
    INSERT INTO sessions (id, user_id, created_at, expires_at, user_agent, ip)
    VALUES (?, ?, ?, ?, ?, ?)
  `).run(id, userId, now, expires, req.headers.get('user-agent'), getIp(req));
  
  return id;
}

export function validateSession(sessionId: string) {
  const row = db.prepare(`
    SELECT user_id, expires_at FROM sessions
    WHERE id = ? AND expires_at > ?
  `).get(sessionId, Date.now()) as any;
  
  return row?.user_id ?? null;
}

Per-request validation: 1 indexed DB query. ~0.5ms di SQLite. Tidak lambat.

Cookie set:

response.cookies.set('session', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  path: '/',
  maxAge: 30 * 24 * 60 * 60, // seconds
});

That’s it. Done.

Cleanup expired sessions

Cron job tiap jam:

DELETE FROM sessions WHERE expires_at < strftime('%s', 'now') * 1000;

3-line maintenance. Vs JWT key rotation yang biasanya complicated.

Kapan JWT masih make sense

  1. Cross-domain auth (SaaS Anda + customer’s subdomain via SSO). Session cookies tidak cross-domain.
  2. Pure stateless microservice dengan trust boundary yang jelas. Indonesian SMB rarely have this.
  3. Mobile API dengan token refresh pattern. Tapi: secure storage di mobile masih bisa simpan session token, jadi tidak strict-strict.

Yang BUKAN reason untuk JWT

  • “Scalable” - session DB query 0.5ms tidak bottleneck
  • “Stateless” - hampir semua app butuh stateful info
  • “Microservice ready” - SMB tidak butuh
  • “Modern” - tidak ada nilai inherent dari “modern” kalau session works

Saran untuk junior dev

Default ke session cookies. Pikirkan JWT cuma kalau ada specific reason. Jangan adopsi pattern karena “industri standard” — banyak industri standard yang dibuat untuk problem yang Anda tidak punya.

Ditulis oleh Reza Pradipta