Jul 25, 2025

RBAC yang Waras (Biar Gak Bocor Data)

Bug permission itu tipe bug yang bikin orang berkeringat tanpa olahraga. Sekali salah, efeknya bisa dari “fitur kok hilang?” sampai “lah ini data siapa?” yang langsung naik level jadi incident.

Maarif Alawi

Full-Stack Web App Developer

RBAC itu mahal kalau salah, karena cost-nya bukan cuma dev time. Cost-nya trust, support chaos, dan rasa was-was tiap deploy. Dalam 3 tahun dan ~13 project shipped, RBAC yang rapi itu salah satu pembeda antara app yang nyaman dipakai vs app yang bikin tim takut buka dashboard.

Cerita singkat: tombol muncul, tapi data ketolak, dan tenant hampir ketuker

Pernah ada kasus user role “Viewer” bisa lihat tombol “Edit” karena UI ngambil state permission dari cache lama. Begitu diklik, API nolak 403, jadi user bingung dan ngegas.

Lebih parahnya, ada query yang hampir ngambil data lintas tenant gara-gara satu endpoint lupa filter tenant_id. Untungnya ketahuan cepat karena log error di Sentry nunjukin request aneh: user A akses project yang bukan tenant-nya.

Lesson-nya jelas: permission harus satu sumber, konsisten antara UI dan API. Dan tenant isolation itu bukan “nanti aja”, itu default hidup.

RBAC Minimal yang Kepake

Tiga role yang biasanya cukup buat banyak produk:

  • Admin: manage users/roles, semua akses, termasuk settings.

  • Editor: bisa create/update data, tapi gak bisa atur role/tenant.

  • Viewer: read-only, gak boleh ngubah apa pun.

Contoh 5 permission yang realistis:

  • projects.read

  • projects.write

  • members.manage

  • settings.update

  • billing.view

Mini permission matrix (kecil aja, tapi jelas):

Permission        Admin   Editor  Viewer
projects.read      ✅       ✅       ✅
projects.write     ✅       ✅       ❌
members.manage     ✅       ❌       ❌
settings.update    ✅       ❌       ❌
billing.view       ✅       ❌       ❌

Kuncinya bukan bikin role banyak-banyak. Kuncinya: permission jelas, naming konsisten, dan gak ada “special case” liar.

UI ≠ Security

Hide button itu cuma UX, bukan security. User masih bisa call API langsung pakai Postman atau script, dan mereka gak peduli tombolmu invis.

Security harus ditegakkan di API dan DB. Kalau kamu pakai Supabase, manfaatkan RLS dan pastikan endpoint server juga ngecek role/permission sebelum query.

Tenant Isolation

Multi-tenant itu intinya satu: semua data punya tenant_id. Rule-nya simpel tapi brutal: setiap query harus selalu filter by tenant.

Jangan berharap “frontend pasti ngirim tenant yang bener”. Ambil tenant dari session/claims, lalu enforce di server dan di RLS.

Kalau ada satu endpoint lupa filter tenant_id, itu cukup buat bikin data bocor. Dan kamu bakal “belajar” dengan cara yang mahal.

Anti-patterns permission yang bikin bahaya

  • Percaya client-side checks doang karena UI “udah ngunci”.
    Solusinya: enforce permission di API + RLS Supabase.

  • Logic permission nyebar di 20 file dengan versi beda-beda.
    Solusinya: satu permission helper, satu mapping role→permissions.

  • Gak ada audit trail untuk perubahan role/akses.
    Solusinya: log aksi sensitif (role update, invite revoke) ke audit_events.

  • Query lupa tenant filter karena “udah aman kok”.
    Solusinya: default scope tenant_id di semua query server, dan tes untuk itu.

Kode: Next.js API route enforce permission + tenant filter

import { NextResponse } from "next/server";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

type Role = "admin" | "editor" | "viewer";
const PERMS: Record<Role, string[]> = {
  admin: ["projects.read", "projects.write", "members.manage", "settings.update", "billing.view"],
  editor: ["projects.read", "projects.write"],
  viewer: ["projects.read"],
};

async function getSession(req: Request) {
  const user_id = req.headers.get("x-user-id");
  const tenant_id = req.headers.get("x-tenant-id");
  const role = (req.headers.get("x-role") as Role) || "viewer";
  return user_id && tenant_id ? { user_id, tenant_id, role } : null;
}

export async function GET(req: Request) {
  const session = await getSession(req);
  if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const canRead = PERMS[session.role]?.includes("projects.read");
  if (!canRead) return NextResponse.json({ error: "Forbidden" }, { status: 403 });

  const { data, error } = await supabase
    .from("projects")
    .select("id,name,status,updated_at")
    .eq("tenant_id", session.tenant_id)
    .order("updated_at", { ascending: false });

  if (error) return NextResponse.json({ error: "Server error" }, { status: 500 });
  return NextResponse.json({ ok: true, data });
}

COPY THIS

  • Role dibatasi sedikit (Admin/Editor/Viewer) dulu, jangan kebanyakan

  • Permission naming konsisten (resource.action)

  • Ada matrix role→permission yang kecil dan jelas

  • UI cuma reflect permission, bukan jadi sumber security

  • API selalu cek permission sebelum query/mutate

  • Semua tabel multi-tenant punya tenant_id

  • Semua query server selalu .eq("tenant_id", tenantId)

  • Tenant diambil dari session/claims, bukan input user

  • Supabase RLS aktif untuk table sensitif

  • Aksi sensitif (role/invite/setting) dicatat di audit log

RBAC yang waras bikin produk aman tanpa bikin dev takut sentuh codebase. Support juga lebih tenang karena masalah akses bisa ditrace, bukan ditebak.

Kalau permission kamu rapi, kamu bisa scaling fitur tanpa scaling rasa panik.

  • ALAWI

    CONTACT

( 00-09 )

CONTACT

Let’s talk about what you’re building
and how I can help.

  • Want to chat fast?

+62 817-7577-7778

  • Prefer email?

maarifalaawi@gmail.com

  • LinkedIn

maarifalawi

Start with a brief.

  • ALAWI

    CONTACT

( 00-09 )

CONTACT

Let’s talk about what you’re building
and how I can help.

  • Want to chat fast?

+62 817-7577-7778

  • Prefer email?

maarifalaawi@gmail.com

  • LinkedIn

maarifalawi

Start with a brief.

  • ALAWI

    CONTACT

( 00-09 )

CONTACT

Let’s talk about what you’re building
and how I can help.

  • Want to chat fast?

+62 817-7577-7778

  • Prefer email?

maarifalaawi@gmail.com

  • LinkedIn

maarifalawi

Start with a brief.

I build and maintain production-ready web apps. Clean UI, stable backend, calm releases.

I build and maintain production-ready web apps. Clean UI, stable backend, calm releases.

I build and maintain production-ready web apps. Clean UI, stable backend, calm releases.

Create a free website with Framer, the website builder loved by startups, designers and agencies.