karawaci.kode

2026-06-06 · 7 min

Bun 1.3 + Supabase Auth: Migrasi SaaS dari Next

Klien SaaS HR (~340 paying company, 4,200 user aktif harian) saya operate sejak 2024. Auth lama: Next.js + Auth.js v5 + custom Postgres adapter. Dua bulan lalu saya migrasi ke Bun + Hono + Supabase Auth. Hasil terukur.

Setup

App lama:

  • Next.js 14 di Vercel Pro
  • Auth.js v5 dengan Postgres adapter
  • Custom JWT signing untuk machine-to-machine
  • Self-hosted email via Postmark

App baru:

  • Bun 1.3 + Hono v4 di Hetzner VPS (cx41, 4 vCPU, 16GB)
  • Supabase Auth (hosted, free tier untuk 50k MAU)
  • JWT verify pakai Supabase JWKS endpoint
  • Email magic link via Supabase (Postmark integration)

Decision driver: cost + cold start. Vercel function cold start untuk auth endpoint pernah hit 3,1 detik di P99, dan login flow Auth.js lumayan boilerplate-heavy.

Cost real (bulanan)

Sebelum:

  • Vercel Pro: $20 base + ~$95 function execution = $115
  • Postmark: $25 (50k email/mo)
  • Postgres Hetzner: $25
  • Total: $165/mo

Sesudah:

  • Hetzner cx41: $25 (sama, hosting Bun app)
  • Supabase paid tier: $25 (free tier MAU cukup, tapi storage + bandwidth bikin auto-upgrade)
  • Postmark: $0 (Supabase yang handle)
  • Total: $50/mo

Saving: $115/mo (~Rp 1,8jt). Annualized: Rp 22jt.

Latency

Login flow end-to-end (user click “login” → dashboard ter-render):

Sebelum:

  • P50: 1,4 detik
  • P95: 2,8 detik (cold start affecting)
  • P99: 4,5 detik

Sesudah:

  • P50: 380ms
  • P95: 720ms
  • P99: 1,1 detik

Cold start hilang karena Bun app long-running di VPS. Supabase Auth verification ~80ms tambahan (JWKS fetch + verify), tapi total tetap jauh lebih cepat.

Yang dirombak (5 hari kerja)

Hari 1: Setup Hono + Bun, basic route, integrate Supabase JS client. Replace Auth.js session cookie dengan Supabase session.

Hari 2: Migrate user data. Pakai Supabase Auth Admin API untuk import 8,400 existing user (dengan hashed password, OAuth provider mapping). Script bash + Bun. Run 47 menit. Verify count match.

Hari 3: Rewrite middleware. Pattern lama:

// Next.js middleware.ts
import { auth } from '@/auth';
export default auth((req) => {
  if (!req.auth) return NextResponse.redirect('/login');
});

Pattern baru:

// Hono middleware
import { createClient } from '@supabase/supabase-js';

app.use('/api/*', async (c, next) => {
  const token = c.req.header('authorization')?.replace('Bearer ', '');
  if (!token) return c.json({ error: 'No auth' }, 401);
  
  const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
  const { data: { user }, error } = await supabase.auth.getUser(token);
  if (error || !user) return c.json({ error: 'Invalid' }, 401);
  
  c.set('user', user);
  await next();
});

Hari 4: RLS migration. Supabase Postgres native RLS. Saya tulis policy untuk 28 table. Test dengan dummy user dari 3 tenant.

Hari 5: E2E test + canary deploy. 5% traffic ke new auth selama 24 jam. Monitor error rate. OK, full cutover.

Yang break

  1. OAuth callback URL: GitHub, Google OAuth app harus diupdate dengan new callback URL. Saya lupa update GitHub OAuth, 12 user login GitHub error selama 40 menit sebelum saya notice. Fix: update + clear comm ke user via in-app notification.

  2. Session migration: User yang active session di Auth.js lama tetap valid sampai expire. Saya tidak invalidate manual. Result: 7 hari first week, user pelan-pelan auto re-login. Saya tambah message: “Login ulang untuk fitur baru.” Adoption 92% dalam 5 hari.

  3. Machine-to-machine token: Existing M2M token (untuk klien yang pakai webhook outbound) pakai signature lama. Saya keep dual-verify (old JWT + new Supabase JWT) selama 30 hari. Setelah itu deprecate. 4 klien aktif refresh token, 0 issue.

  4. Bun + Supabase JS bug: Supabase JS v2.45 punya bug di Bun runtime untuk WebSocket realtime. Saya tidak pakai realtime untuk auth, jadi skip. Tapi catat untuk future.

Memory

Hetzner VPS RSS:

  • Bun + Hono app: 145MB stable
  • Postgres (auth tables migrated to Supabase): 280MB → 90MB (lebih kecil setelah auth offload)

Total memory usage VPS turun 35%. Bisa downsize ke cx31 ($15/mo) kalau klien mau, tapi saya rekomendasi keep cx41 untuk headroom.

Yang Supabase Auth tidak kasih

  1. Custom claim di JWT: bisa, tapi via Postgres function. Lebih complex dari Auth.js callback pattern.
  2. Multi-tenancy native: Supabase Auth single user pool. Kalau butuh hard tenant isolation di level auth: butuh extra work via RLS atau separate Supabase project.
  3. Granular session management: list active session per user, revoke specific session — limited. Auth.js dulu saya bisa build sendiri di Postgres.

Untuk klien saya: tidak ada blocker. Tapi worth check requirement Anda.

Verdict

Untuk SaaS Indonesia dengan SMB scale dan cost-sensitive: kombinasi Bun + Hono + Supabase Auth sangat compelling. Cost turun 70%, latency turun 70%, security model lebih audit-able.

Bukan untuk fintech yang butuh fully isolated auth stack (audit trail wajib in-house) atau app dengan custom auth flow yang Supabase tidak support (passkey + biometric advanced).

Migration cost (5 hari + 2 minggu monitor): Rp 12jt billed ke klien. Break-even di 7 bulan dari hosting saving. Sekarang sudah 2 bulan, on track.

Bukan magic, butuh perencanaan migration data + dual-verify period. Worth-it untuk most SMB SaaS.

Ditulis oleh Reza Pradipta