Pengembangan Backend

Studi Kasus Migrasi Backend ke Supabase: Lebih Cepat dan Aman

M
MUGHU
24 menit baca
Studi Kasus Migrasi Backend ke Supabase: Lebih Cepat dan Aman
Daftar isi

Pernah nggak sih ngerasa aplikasi yang tadinya simpel, tiba-tiba jadi ribet banget karena harus nambahin fitur login, simpan file, data realtime, sampai aturan akses yang harus super ketat? Pas momen itu, ngurusin backend rasanya kayak ngerakit lemari tapi nggak ada buku panduannya: semua bahannya ada, tapi bingung bautnya harus ditaruh di mana.

Mungkin kita bisa bikin server, API, database, sama sistem autentikasi sendiri-sendiri, tapi jujur aja, waktu kita bakal habis cuma buat nyambungin semuanya.

Nah, di tulisan ini, aku mau bahas soal aplikasi task manager internal yang memutuskan buat pindahin backend-nya ke Supabase. Fokusnya bukan buat nyari tools yang paling keren, tapi buat ngebuktiin: apakah PostgreSQL yang udah dikelola, API otomatis, dan Row Level Security (RLS) bisa bikin proses coding jadi jauh lebih cepet, tanpa harus ngorbanin keamanan data kita?

Latar belakang aplikasi dan kondisi awal

Studi kasus ini menggunakan prototipe aplikasi pengelola tugas internal tanpa nama perusahaan fiktif. Aplikasi dipakai sebagai skenario terkontrol untuk mengukur dampak keputusan arsitektur, bukan sebagai klaim bahwa semua proyek akan memperoleh angka yang sama.

Kebutuhan aplikasinya cukup umum. Pengguna harus dapat membuat akun, masuk, menyimpan profil, membuat tugas, mengubah status, menambahkan tenggat, dan melihat perubahan tanpa selalu memuat ulang halaman.

Versi awal dibangun dengan beberapa layanan terpisah:

  • Database PostgreSQL terkelola

  • API Node.js

  • Sistem autentikasi

  • Object storage

  • WebSocket server

  • Script migration

  • Middleware authorization

Pilihan tersebut tidak salah. Bahkan, untuk aplikasi dengan kebutuhan sangat khusus, pemisahan layanan dapat memberi kontrol yang lebih besar.

Masalahnya muncul karena proyek dikerjakan oleh tim kecil dengan target peluncuran cepat. Banyak waktu habis bukan untuk fitur pengguna, melainkan untuk merawat sambungan antarbagian.

Setiap fitur sederhana memiliki jalur panjang. Ketika pengguna membuat tugas, frontend memanggil API, API memvalidasi token, middleware memeriksa akses, ORM menjalankan query, lalu server mengembalikan respons. Untuk pembaruan langsung, perubahan yang sama juga harus dikirim melalui WebSocket.

Arsitektur awal terlihat seperti ini:

TEXT
Frontend
   |
   +-- Auth provider
   |
   +-- REST API
          |
          +-- Middleware authorization
          |
          +-- PostgreSQL
          |
          +-- Object storage
          |
          +-- WebSocket server

Proyek sudah memiliki dasar yang masuk akal, tetapi biaya koordinasinya tinggi. Satu perubahan schema dapat menyentuh migration, model ORM, validator, route API, tipe frontend, dan dokumentasi endpoint.

Setelah beberapa iterasi, tim mengevaluasi Supabase Database. Setiap proyek Supabase menyediakan database PostgreSQL khusus dengan API otomatis, kontrol akses berbasis Postgres, integrasi Auth, Realtime, dan Storage.

Supabase Database bukan database baru yang meniru PostgreSQL. Intinya tetap Postgres, lalu Supabase menambahkan layanan terintegrasi di sekelilingnya.

Fakta ini penting karena migrasi tidak mengharuskan tim meninggalkan SQL relasional. Foreign key, index, transaction, function, trigger, enum, view, dan constraint tetap dapat digunakan.

Supabase juga bersifat open-source dan repositorinya tersedia melalui GitHub resmi Supabase. Tim dapat memeriksa komponen, menjalankan lingkungan lokal, atau mempertimbangkan self-hosting jika kebutuhan operasional berubah.

Keputusan awal bukan “pindahkan semuanya”. Tim memilih satu alur kecil sebagai percobaan:

  1. Pengguna mendaftar.

  2. Profil dibuat.

  3. Pengguna membuat tugas.

  4. Hanya pemilik yang dapat membaca tugas.

  5. Status tugas dapat diperbarui.

  6. Perubahan muncul secara realtime.

  7. Lampiran disimpan secara privat.

Alur tersebut cukup kecil untuk diuji dalam beberapa hari, tetapi cukup lengkap untuk memperlihatkan masalah nyata pada schema, autentikasi, authorization, Realtime, dan Storage.

Tantangan yang membuat backend awal sulit dipertahankan

Hambatan terbesar bukan performa database. PostgreSQL sudah mampu menangani workload prototipe dengan nyaman.

Masalah utamanya adalah jumlah lapisan yang harus dijaga agar tetap konsisten. Ketika aturan bisnis berubah, setiap lapisan perlu diperiksa satu per satu.

Contohnya, tugas semula hanya memiliki status todo dan done. Kemudian muncul kebutuhan in_progress.

Perubahan kecil itu memerlukan:

  • Migration database

  • Pembaruan enum backend

  • Pembaruan validator API

  • Pembaruan tipe frontend

  • Penyesuaian filter

  • Penyesuaian event WebSocket

  • Penambahan test

  • Pembaruan dokumentasi

Tidak semua perubahan selalu dilakukan pada commit yang sama. Akibatnya, frontend kadang mengirim status baru sebelum API menerimanya, atau API sudah menerima nilai baru saat UI belum tahu cara menampilkannya.

Tantangan kedua adalah authorization. Middleware API memeriksa bahwa pengguna sudah login, tetapi pemeriksaan kepemilikan record harus ditulis pada setiap endpoint.

Query aman terlihat seperti ini:

SQL
select *
from tasks
where id = $1
  and user_id = $2;

Masalah muncul ketika developer menambahkan endpoint baru dan lupa syarat user_id. Database tetap mengembalikan record karena aturan kepemilikan hanya hidup di kode aplikasi.

Bug seperti ini tidak selalu terlihat pada pengujian biasa. Pengguna mengakses datanya sendiri, sehingga halaman tampak bekerja. Kebocoran baru muncul jika seseorang mencoba ID milik pengguna lain.

Tantangan ketiga adalah pengelolaan kredensial. Frontend membutuhkan konfigurasi publik, API membutuhkan secret Auth, backend membutuhkan password database, dan WebSocket server membutuhkan akses tambahan.

Semakin banyak secret, semakin banyak pula tempat yang harus dikonfigurasi:

TEXT
Development
Staging
Production
CI
Preview deployment
Mesin developer
Worker
WebSocket server

Satu variabel yang salah dapat membuat lingkungan staging terhubung ke layanan development. Lebih buruk lagi, secret backend dapat masuk ke bundle frontend jika penamaannya keliru.

Tantangan keempat adalah realtime. Server WebSocket dapat mengirim pembaruan, tetapi authorization event perlu mengikuti authorization data.

Jika pengguna hanya boleh membaca tugas sendiri, server tidak boleh menyiarkan seluruh perubahan tabel lalu mengandalkan frontend untuk menyembunyikannya. Data sudah telanjur keluar dari server.

Tantangan kelima adalah perubahan schema. Dashboard database berguna untuk eksplorasi, tetapi perubahan manual membuat development, staging, dan production mudah berbeda.

Tim pernah menghadapi situasi seperti ini:

TEXT
Development: kolom due_at tersedia
Staging: kolom due_at tersedia tetapi nullable
Production: kolom due_at belum tersedia

Kode frontend sudah mencoba membaca due_at, sementara production belum memiliki kolomnya. Hasilnya bukan drama besar ala film bencana, tetapi cukup untuk membuat sore yang tenang berubah menjadi sesi membaca log.

Masalah berikutnya adalah observability. Ketika query gagal, pesan yang terlihat sering hanya “Failed to fetch tasks”.

Penyebab sebenarnya bisa bermacam-macam:

  • Token kedaluwarsa

  • Policy menolak akses

  • Constraint gagal

  • Nama kolom salah

  • Koneksi database penuh

  • Server API bermasalah

  • WebSocket terputus

  • Schema antarlingkungan berbeda

Tanpa correlation ID dan log yang konsisten, investigasi memerlukan membuka beberapa dashboard sekaligus.

Ada pula keterbatasan waktu. Tim ingin menguji fitur produk, bukan menghabiskan seluruh sprint untuk membuat infrastruktur internal yang belum tentu menjadi pembeda produk.

Target percobaan kemudian ditetapkan:

Sasaran

Kondisi awal

Target percobaan

Waktu membuat backend fitur tugas

4–5 hari

Maksimal 2 hari

Jumlah lapisan authorization

API dan query

Database sebagai pengaman utama

Endpoint CRUD manual

5 endpoint

Tidak ada endpoint CRUD khusus

Waktu setup developer baru

3–4 jam

Di bawah 1 jam

Test isolasi pengguna

Sebagian manual

Otomatis untuk semua operasi

Layanan realtime khusus

1 server

Tidak ada server khusus

Sumber schema

Campuran

Migration dalam Git

Angka ini merupakan baseline proyek studi kasus, bukan jaminan hasil universal. Proyek yang sudah memiliki backend matang mungkin tidak mendapat penghematan serupa.

Pendekatan migrasi yang dipilih

Tim tidak melakukan rewrite besar. Pendekatannya adalah mengganti satu alur vertikal sambil mempertahankan frontend dan proses deployment yang sudah ada.

Alur vertikal berarti satu fitur dipindahkan secara lengkap: database, akses, autentikasi, query client, realtime, file, dan pengujian. Strategi ini lebih mudah dinilai daripada memindahkan semua tabel tetapi belum ada satu pun fitur yang selesai.

Prinsip yang digunakan adalah:

  1. PostgreSQL tetap menjadi sumber kebenaran.

  2. RLS menjadi batas keamanan utama.

  3. Frontend hanya memakai publishable key.

  4. Operasi berhak tinggi tetap berada di server.

  5. Semua schema disimpan sebagai migration.

  6. Setiap policy diuji memakai dua akun.

  7. Realtime hanya dinyalakan untuk kebutuhan nyata.

  8. Hasil diukur sebelum keputusan perluasan.

Tim mempelajari dokumentasi resmi Supabase Database sebelum menulis schema. Fokus awalnya bukan seluruh fitur platform, melainkan Database, Auth, RLS, CLI, dan Realtime.

Schema dirancang dengan tiga entitas:

Entitas

Tujuan

Pemilik

profiles

Data profil aplikasi

Pengguna

tasks

Tugas dan status

Pengguna

task_attachments

Metadata lampiran

Pengguna melalui tugas

Identitas tetap berada pada auth.users. Tabel profiles memakai ID yang sama agar hubungan mudah dipahami.

Schema pertama dibuat seperti ini:

SQL
create type public.task_status as enum (
  'todo',
  'in_progress',
  'done'
);

create table public.profiles (
  id uuid primary key
    references auth.users(id)
    on delete cascade,

  display_name text not null
    check (char_length(display_name) between 2 and 80),

  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

create table public.tasks (
  id uuid primary key default gen_random_uuid(),

  user_id uuid not null
    references auth.users(id)
    on delete cascade,

  title text not null
    check (char_length(title) between 1 and 200),

  description text,

  status public.task_status not null default 'todo',

  due_at timestamptz,

  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

Constraint dipakai untuk melindungi aturan yang tidak boleh bergantung pada frontend. Judul wajib diisi, status harus dikenali, dan pemilik harus menunjuk pengguna yang valid.

Tim menambahkan index untuk query paling umum:

SQL
create index tasks_user_created_idx
on public.tasks (user_id, created_at desc);

create index tasks_user_status_idx
on public.tasks (user_id, status);

Index tersebut mendukung daftar tugas terbaru dan filter status. Tim tidak menambahkan index pada setiap kolom karena index juga memiliki biaya saat data ditulis.

RLS kemudian diaktifkan:

SQL
alter table public.profiles enable row level security;
alter table public.tasks enable row level security;

Policy dibuat per operasi, bukan satu aturan luas untuk semuanya:

SQL
create policy "Users can read own tasks"
on public.tasks
for select
to authenticated
using ((select auth.uid()) = user_id);
SQL
create policy "Users can create own tasks"
on public.tasks
for insert
to authenticated
with check ((select auth.uid()) = user_id);
SQL
create policy "Users can update own tasks"
on public.tasks
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
SQL
create policy "Users can delete own tasks"
on public.tasks
for delete
to authenticated
using ((select auth.uid()) = user_id);

using memeriksa record yang menjadi target. with check memeriksa record setelah perubahan.

Pemisahan ini mencegah pengguna mengubah user_id tugas sendiri menjadi ID pengguna lain. Tanpa with check, aturan update dapat terlihat aman tetapi masih menyisakan celah kepemilikan.

Tim sengaja tidak memakai using (true) pada data privat. Policy seperti itu cepat membuat demo bekerja, tetapi semua pengguna dalam role tersebut dapat mengakses seluruh baris.

Frontend kemudian dihubungkan melalui supabase-js:

BASH
npm install @supabase/supabase-js

Konfigurasi browser:

ENV
VITE_SUPABASE_URL=https://PROJECT_REF.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY

Client dibuat dengan kode sederhana:

TS
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  import.meta.env. VITE_SUPABASE_URL,
  import.meta.env. VITE_SUPABASE_PUBLISHABLE_KEY,
)

Publishable key memang dapat dilihat pengguna. Keamanan tetap berasal dari sesi Auth dan policy database.

service_role tidak dimasukkan ke frontend. Operasi administratif, seperti menghapus akun atau menjalankan proses lintas pengguna, tetap berada pada backend tepercaya.

Pendekatan ini mengurangi jumlah API CRUD manual. Frontend dapat meminta data langsung melalui Data API, sedangkan PostgreSQL memutuskan baris mana yang boleh keluar.

Implementasi Supabase Database pada alur tugas

Implementasi dimulai dari lingkungan lokal. Supabase CLI dipasang sebagai dependensi development:

BASH
npm install supabase --save-dev
npx supabase init
npx supabase start

Semua perubahan schema dibuat dalam migration:

BASH
npx supabase migration new create_task_schema

File SQL disimpan pada:

TEXT
supabase/migrations/

Setiap perubahan diuji dengan reset:

BASH
npx supabase db reset

Langkah ini terlihat sederhana, tetapi dampaknya besar. Schema tidak lagi bergantung pada ingatan tentang tombol apa yang pernah diklik di dashboard.

Developer baru cukup menarik repository, menjalankan stack lokal, lalu menerapkan migration. Jika reset gagal, masalah ditemukan sebelum perubahan mencapai staging.

Profil pengguna dibuat melalui trigger setelah akun Auth berhasil dibuat:

SQL
create or replace function public.handle_new_user()
returns trigger
language plpgsql
security definer
set search_path = ''
as $$
begin
  insert into public.profiles (
    id,
    display_name
  )
  values (
    new.id,
    coalesce(
      new.raw_user_meta_data ->> 'display_name',
      'Pengguna baru'
    )
  );

  return new;
end;
$$;

Trigger dipasang pada auth.users:

SQL
create trigger on_auth_user_created
after insert on auth.users
for each row
execute function public.handle_new_user();

Function dibuat sesempit mungkin. Tim tidak menaruh proses berat atau panggilan eksternal di dalam trigger karena kegagalan function dapat menggagalkan pendaftaran.

Frontend mendaftarkan pengguna:

TS
const { data, error } = await supabase.auth.signUp({
  email,
  password,
  options: {
    data: {
      display_name: displayName,
    },
  },
})

Setelah login, daftar tugas diambil dengan kolom terbatas:

TS
const { data: tasks, error } = await supabase
  .from('tasks')
  .select('id, title, status, due_at, created_at')
  .eq('user_id', user.id)
  .order('created_at', { ascending: false })
  .limit(20)

Filter user_id membantu query planner. RLS tetap diperlukan karena pengguna dapat menghapus filter dari request yang dibuat sendiri.

Tugas dibuat dengan ID pengguna dari sesi:

TS
const {
  data: { user },
} = await supabase.auth.getUser()

if (!user) {
  throw new Error('Pengguna belum login')
}

const { data, error } = await supabase
  .from('tasks')
  .insert({
    user_id: user.id,
    title,
    status: 'todo',
  })
  .select()
  .single()

Jika pengguna mencoba memakai ID orang lain, policy insert menolak request.

Pembaruan status tidak membutuhkan endpoint khusus:

TS
const { data, error } = await supabase
  .from('tasks')
  .update({
    status: 'done',
  })
  .eq('id', taskId)
  .select()
  .single()

Policy update memastikan record lama dimiliki pengguna dan nilai baru tetap memiliki pemilik yang sama.

Realtime hanya diaktifkan pada tabel tasks. Client berlangganan perubahan:

TS
const channel = supabase
  .channel('task-changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'tasks',
    },
    (payload) => {
      refreshVisibleTasks(payload)
    },
  )
  .subscribe()

Subscription dibersihkan ketika halaman ditutup:

TS
await supabase.removeChannel(channel)

Tim tidak mengaktifkan Realtime pada profiles karena perubahan nama pengguna tidak membutuhkan pembaruan seketika. Satu keputusan kecil ini menjaga implementasi tetap sederhana.

Lampiran disimpan dalam bucket privat task-attachments. Struktur path memakai pemilik dan tugas:

TEXT
USER_ID/TASK_ID/FILE_NAME

Upload dilakukan melalui client:

TS
const path = `${user.id}/${taskId}/${file.name}`

const { data, error } = await supabase.storage
  .from('task-attachments')
  .upload(path, file, {
    upsert: false,
    cacheControl: '3600',
  })

Policy Storage membatasi akses berdasarkan bucket dan folder pengguna. File tidak dijadikan publik hanya untuk mempermudah pengambilan URL.

Tipe TypeScript dibuat dari schema:

BASH
npx supabase gen types typescript \
  --local \
  > src/types/database.ts

Client kemudian menggunakan tipe tersebut:

TS
import type { Database } from './types/database'

export const supabase = createClient<Database>(
  import.meta.env. VITE_SUPABASE_URL,
  import.meta.env. VITE_SUPABASE_PUBLISHABLE_KEY,
)

Perubahan enum atau nama kolom kini lebih cepat terdeteksi oleh TypeScript. Bug tidak harus menunggu sampai request dijalankan.

Pengujian akses memakai dua pengguna. Setiap operasi diperiksa pada record sendiri dan record orang lain.

Matriks uji terlihat seperti ini:

Skenario

Hasil yang diharapkan

Alice membaca tugas Alice

Diizinkan

Alice membaca tugas Bob

Ditolak

Bob memperbarui tugas Bob

Diizinkan

Bob memperbarui tugas Alice

Ditolak

Pengguna anonim membaca tugas

Ditolak

Alice membuat tugas untuk Bob

Ditolak

Alice memindahkan tugas ke Bob

Ditolak

Test ini dijalankan setiap kali policy berubah. Tim juga menguji bahwa frontend tidak memuat service_role, Storage menolak folder pengguna lain, dan subscription Realtime tidak menampilkan data lintas akun.

Observability ditambahkan di sekitar query penting:

TS
const startedAt = performance.now()

const { data, error } = await supabase
  .from('tasks')
  .select('id, title, status')
  .eq('user_id', user.id)
  .limit(20)

console.info('tasks.list', {
  durationMs: Math.round(performance.now() - startedAt),
  rowCount: data?.length ?? 0,
  errorCode: error?.code ?? null,
})

Log tidak memuat token, password database, secret, atau isi tugas. Detail sensitif bukan bonus observability; itu calon masalah baru.

Sebelum deployment, migration diterapkan pada staging. Tim memeriksa query plan, policy, Realtime, dan Storage menggunakan data sintetis dengan volume lebih besar.

Perubahan production dilakukan bertahap. Kolom baru dibuat nullable lebih dahulu, kode kompatibel diterapkan, data lama diisi, lalu constraint ditambahkan setelah verifikasi.

Hasil setelah migrasi alur pertama

Percobaan berlangsung selama empat minggu, termasuk pembuatan schema, integrasi frontend, pengujian, perbaikan policy, dan pengamatan pada staging. Angka berikut berasal dari implementasi terkontrol ini, bukan benchmark resmi Supabase.

Hasil utama:

Metrik

Sebelum

Setelah

Perubahan

Waktu backend fitur tugas

4,5 hari

1,8 hari

Turun 60%

Endpoint CRUD khusus

5

0

Turun 100%

Waktu setup developer

3,5 jam

45 menit

Turun 79%

File authorization aplikasi

6

2 migration policy

Lebih terpusat

Layanan realtime khusus

1

0

Dihapus

Skenario isolasi otomatis

4

14

Naik 250%

Bug perbedaan schema per sprint

3

0

Tidak muncul

Median waktu baca 20 tugas

118 ms

74 ms

Turun 37%

Penurunan waktu pengembangan bukan hanya karena query client lebih pendek. Tim tidak lagi membuat route, controller, validator, serializer, dan dokumentasi CRUD untuk operasi standar.

API otomatis mengikuti schema. TypeScript dihasilkan dari database, sehingga kontrak data tidak perlu ditulis ulang secara manual.

Pengurangan endpoint khusus bukan berarti seluruh backend hilang. Operasi rahasia dan proses bisnis kompleks tetap membutuhkan server atau Edge Function.

Alur seperti pembayaran, webhook, email, dan tugas administratif tidak dipindahkan ke browser. Supabase mengurangi boilerplate, bukan menghapus kebutuhan untuk berpikir.

Waktu setup developer turun karena lingkungan lokal dibangun dari repository:

BASH
npm install
npx supabase start
npx supabase db reset
npm run dev

Developer tidak perlu meminta dump database manual atau menebak perubahan schema yang pernah dibuat melalui dashboard.

Hasil keamanan juga lebih mudah dilihat. Sebelumnya, tim memeriksa kepemilikan pada endpoint. Setelah migrasi, aturan kepemilikan ditempatkan pada database dan diuji pada semua operasi.

Perubahan paling penting bukan jumlah kode yang berkurang, melainkan aturan akses menjadi lebih dekat dengan data yang dilindungi.

Pada percobaan penetrasi internal sederhana, request yang mengubah ID tugas atau user_id tidak berhasil melewati policy. Menghapus filter frontend juga tidak membuka record pengguna lain.

Realtime menghapus kebutuhan server WebSocket khusus untuk tabel tugas. Waktu implementasi pembaruan langsung turun dari sekitar satu hari menjadi kurang dari tiga jam, termasuk cleanup subscription dan pengujian dua akun.

Storage privat membutuhkan waktu lebih lama daripada membuat bucket publik. Namun, hasilnya sesuai kebutuhan: file hanya tersedia melalui akses yang dikendalikan, bukan URL permanen yang dapat dibagikan tanpa batas.

Performa query membaik setelah tim menambahkan index gabungan dan filter kepemilikan. Perbaikan tersebut berasal dari desain PostgreSQL yang baik, bukan sihir dashboard.

Query plan menunjukkan index digunakan pada daftar tugas terbaru:

SQL
explain analyze
select id, title, status
from public.tasks
where user_id = 'USER_UUID'
order by created_at desc
limit 20;

Egress pada halaman daftar turun sekitar 31% setelah .select('*') diganti dengan pemilihan kolom eksplisit. Deskripsi panjang tidak lagi dikirim sebelum pengguna membuka detail tugas.

Ada juga hasil yang tidak sepenuhnya positif. Debugging RLS membutuhkan penyesuaian karena query yang ditolak kadang terlihat seperti data kosong.

Tim membuat prosedur debug:

  1. Pastikan sesi tersedia.

  2. Periksa role request.

  3. Jalankan query sebagai pengguna terkait.

  4. Baca policy select.

  5. Periksa policy operasi lain.

  6. Pastikan index tersedia.

  7. Bandingkan schema lokal dan remote.

Migration juga menambah disiplin. Perubahan cepat melalui dashboard terasa menggoda, tetapi tim sepakat bahwa perubahan production harus berasal dari file SQL dalam Git.

Biaya operasional belum menjadi tolok ukur utama karena skala percobaan kecil. Tim tetap memantau database, egress, Storage, koneksi Realtime, dan invocation Functions agar biaya tidak baru diperhatikan saat tagihan mulai punya kepribadian.

Hasil keseluruhan menunjukkan bahwa Supabase cocok untuk alur ini karena tiga alasan:

  • Data bersifat relasional.

  • Tim sudah memahami SQL.

  • Banyak operasi merupakan CRUD dengan aturan kepemilikan yang jelas.

Jika aplikasi membutuhkan kontrol jaringan sangat khusus, database nonrelasional, atau proses backend berat pada setiap request, hasilnya dapat berbeda.

Pelajaran yang dapat diterapkan pada proyek lain

Pelajaran pertama adalah mulai dari schema, bukan dashboard. Supabase memberi antarmuka yang nyaman, tetapi kualitas aplikasi tetap bergantung pada struktur PostgreSQL.

Tentukan entitas, hubungan, constraint, index, dan perilaku penghapusan sebelum menghubungkan frontend. API otomatis hanya sebaik schema yang menjadi dasarnya.

Pelajaran kedua adalah RLS perlu dirancang, bukan sekadar diaktifkan. Tombol “Enable RLS” tidak langsung menghasilkan authorization yang benar.

Setiap operasi memerlukan pertanyaan berbeda:

  • Baris mana yang boleh dibaca?

  • Nilai apa yang boleh dimasukkan?

  • Record lama mana yang boleh diperbarui?

  • Seperti apa nilai setelah update?

  • Siapa yang boleh menghapus?

  • Apa yang terjadi ketika role berubah?

Policy select saja tidak cukup untuk aplikasi CRUD. insert, update, dan delete perlu diuji secara terpisah.

Pelajaran ketiga adalah publishable key bukan secret. Key tersebut berada di client, sehingga perlindungan harus tetap bekerja ketika pengguna membuat request sendiri.

Sebaliknya, service_role benar-benar rahasia. Jangan memasukkannya ke frontend hanya karena satu query sulit melewati policy. Perbaiki desain akses atau pindahkan operasi ke server tepercaya.

Pelajaran keempat adalah jangan memindahkan semua logika ke database. Constraint, RLS, trigger singkat, dan function transaksional cocok berada dekat data.

Proses panjang, panggilan API eksternal, pembayaran, dan workflow dengan retry lebih mudah dikelola melalui backend atau job. Database bukan tempat terbaik untuk mengirim email sambil berharap jaringan selalu ramah.

Pelajaran kelima adalah migration lebih penting daripada kecepatan klik. Table Editor membantu eksplorasi, tetapi Git memberi riwayat yang dapat ditinjau.

Workflow yang dipertahankan tim:

TEXT
Ubah schema lokal
→ buat migration
→ reset database
→ jalankan test
→ generate tipe
→ deploy ke staging
→ verifikasi
→ backup production
→ deploy

Pelajaran keenam adalah Realtime harus punya alasan. Tidak semua tabel perlu disiarkan.

Aktifkan hanya untuk pengalaman yang benar-benar membutuhkan pembaruan langsung. Daftar referensi yang berubah sebulan sekali tidak perlu koneksi WebSocket yang setia menunggu seperti satpam shift malam.

Pelajaran ketujuh adalah ukur hasil per alur, bukan per teknologi. Jumlah fitur Supabase bukan bukti bahwa migrasi berhasil.

Tim mengukur waktu pembuatan fitur, setup developer, jumlah endpoint, skenario test, latensi, egress, dan insiden schema. Metrik tersebut langsung berkaitan dengan pekerjaan.

Pelajaran kedelapan adalah open-source tidak berarti bebas operasi. Supabase dapat diperiksa dan di-host sendiri, tetapi self-hosting memindahkan tanggung jawab backup, upgrade, monitoring, keamanan, dan kapasitas ke tim.

Hosted Supabase dipilih pada studi kasus ini karena tujuannya mengurangi beban operasional. Self-hosting baru masuk akal jika kebutuhan kontrol benar-benar sebanding dengan biaya pengelolaannya.

Pelajaran kesembilan adalah integrasi pihak ketiga tetap perlu prinsip hak minimum. Klaim bahwa key dienkripsi tidak otomatis membuat pemberian service_role aman.

Jika sebuah alat hanya perlu membaca satu tabel, buat jalur akses terbatas. Jangan memberikan kunci induk hanya karena integrasinya ingin selesai sebelum makan siang.

Pelajaran kesepuluh adalah PostgreSQL tetap perlu dipelajari. Supabase membuat banyak hal lebih mudah, tetapi index, transaction, lock, query plan, constraint, dan tipe data tetap menentukan kualitas backend.

Gunakan dokumentasi PostgreSQL ketika menghadapi perubahan schema, query lambat, function, atau perilaku transaksi. Memahami lapisan dasarnya membuat dashboard terasa seperti alat bantu, bukan kotak misterius.

Studi kasus ini memperlihatkan bahwa Supabase Database dapat memangkas pekerjaan backend standar ketika schema relasional, RLS, migration, dan pengujian dirancang dengan disiplin. Ambil satu alur pengguna yang kecil, ukur kondisi awalnya, lalu uji Supabase sampai keamanan dan hasilnya dapat dibuktikan—bukan sekadar terlihat berhasil di demo.

Keputusan fase kedua setelah pilot

Setelah alur tugas stabil, tim tidak langsung memindahkan seluruh backend. Hasil pilot memang positif, tetapi perluasan tanpa batas dapat mengubah migrasi bertahap menjadi rewrite besar dengan logo baru.

Tim memakai matriks keputusan untuk memilih fitur berikutnya:

Kandidat fitur

Nilai migrasi

Risiko

Keputusan

Komentar tugas

Tinggi

Rendah

Pindahkan

Notifikasi aktivitas

Sedang

Sedang

Uji terbatas

Laporan mingguan

Sedang

Rendah

Tetap di job backend

Integrasi pembayaran

Rendah

Tinggi

Tetap di server

Pencarian lampiran

Sedang

Sedang

Tunda

Dashboard admin

Tinggi

Tinggi

Rancang policy dahulu

Komentar tugas dipilih karena pola datanya mirip dengan tugas: setiap komentar memiliki penulis, terkait dengan satu tugas, dan hanya boleh dibaca oleh pengguna yang memiliki akses ke tugas tersebut.

Schema komentar dibuat dengan hubungan yang jelas:

SQL
create table public.task_comments (
  id uuid primary key default gen_random_uuid(),

  task_id uuid not null
    references public.tasks(id)
    on delete cascade,

  author_id uuid not null
    references auth.users(id)
    on delete cascade,

  body text not null
    check (char_length(body) between 1 and 2000),

  created_at timestamptz not null default now()
);

Policy komentar lebih rumit daripada policy tugas. Kepemilikan komentar saja tidak cukup karena pengguna juga harus memiliki akses ke tugas induk.

Tim membuat function authorization pada schema privat agar logika tidak tersebar di beberapa policy:

SQL
create schema if not exists private;

create or replace function private.can_access_task(
  requested_task_id uuid
)
returns boolean
language sql
security definer
set search_path = ''
stable
as $$
  select exists (
    select 1
    from public.tasks
    where id = requested_task_id
      and user_id = (select auth.uid())
  );
$$;

Policy baca komentar memakai function tersebut:

SQL
create policy "Users can read comments on accessible tasks"
on public.task_comments
for select
to authenticated
using (
  (select private.can_access_task(task_id))
);

Function security definer ditempatkan pada schema yang tidak terekspos. Hak eksekusi juga dibatasi agar function tidak berubah menjadi pintu samping yang lebih lebar daripada pintu depan.

Tim menguji tiga kondisi tambahan:

  • Pengguna dapat membaca komentar pada tugas sendiri.

  • Pengguna tidak dapat membaca komentar pada tugas orang lain.

  • Pengguna tidak dapat memindahkan komentar ke tugas yang tidak dapat diakses.

Waktu implementasi komentar tercatat 1,2 hari. Pada arsitektur lama, estimasi awalnya 2,5 hari karena perlu route, controller, validator, event WebSocket, dan pemeriksaan akses tambahan.

Penghematan ini memperkuat keputusan untuk memakai Supabase pada fitur CRUD relasional. Meski begitu, pembayaran tetap berada pada backend. Secret penyedia pembayaran, webhook, idempotency, dan retry tidak cocok dipindahkan ke browser hanya demi mengurangi satu endpoint.

Guardrail untuk menjaga migrasi tetap sehat

Fase kedua memperkenalkan aturan bahwa setiap tabel baru harus memiliki pemilik teknis, model akses, dan rencana pemulihan. Tabel yang belum memiliki ketiganya tidak boleh masuk production.

Template review dibuat seperti ini:

TEXT
Nama tabel:
Tujuan:
Pemilik teknis:
Sumber data:
Jenis data sensitif:
Role yang dapat membaca:
Role yang dapat menulis:
Policy SELECT:
Policy INSERT:
Policy UPDATE:
Policy DELETE:
Index utama:
Retensi:
Backup:
Prosedur penghapusan:

Review ini memaksa tim membahas hal yang biasanya baru muncul saat ada masalah. Pertanyaan sederhana seperti “siapa boleh menghapus?” sering membuka kebutuhan audit yang belum terpikirkan.

Tim juga menetapkan batas untuk penggunaan trigger. Trigger hanya dipakai ketika aturan harus berlaku pada semua jalur penulisan dan prosesnya cepat.

Trigger tidak dipakai untuk:

  • Menghubungi API eksternal

  • Mengirim email

  • Memproses file besar

  • Membuat laporan

  • Menjalankan workflow panjang

  • Menunggu layanan pihak ketiga

Pekerjaan tersebut dikirim ke backend atau sistem job. Database tetap fokus menjaga integritas dan transaksi.

Untuk perubahan schema, tim memakai pola expand and contract. Kolom atau tabel baru ditambahkan terlebih dahulu, kode lama dan baru berjalan bersamaan, data dipindahkan, lalu bagian lama dihapus pada rilis terpisah.

Pola ini membuat deployment sedikit lebih panjang, tetapi menurunkan risiko downtime. Mengganti kolom secara mendadak memang terasa tegas, tetapi database production biasanya kurang menghargai sikap dramatis.

Sebelum migration dijalankan, tim memeriksa panduan database Supabase dan dokumentasi PostgreSQL untuk jenis perubahan yang digunakan. Operasi ALTER TABLE tertentu dapat mengambil lock atau memindai seluruh tabel.

Pengukuran setelah fitur komentar ditambahkan

Tim mempertahankan dashboard metrik yang sama agar hasil fase kedua dapat dibandingkan dengan pilot.

Metrik

Target

Hasil fase kedua

Waktu implementasi komentar

Di bawah 2 hari

1,2 hari

Kebocoran lintas pengguna

0

0

Policy regression test

Minimal 12

18

Median query 20 komentar

Di bawah 100 ms

61 ms

Error rate staging

Di bawah 1%

0,4%

Query tanpa index pada jalur utama

0

0

Secret pada bundle frontend

0

0

Median query diukur pada staging dengan data sintetis, bukan database kosong. Tim membuat ribuan komentar agar query plan lebih mendekati kondisi yang mungkin muncul setelah aplikasi berkembang.

Satu masalah ditemukan pada implementasi awal. Halaman detail tugas memanggil query komentar dua kali karena subscription Realtime dibuat ulang setiap kali state filter berubah.

Dampaknya belum besar, tetapi log menunjukkan jumlah channel terus naik selama pengujian panjang. Cleanup subscription kemudian dipindahkan ke lifecycle yang tepat, dan nama channel dibuat stabil per tugas.

Setelah perbaikan, jumlah koneksi kembali sesuai jumlah halaman aktif. Temuan ini menjadi pengingat bahwa layanan terkelola tetap membutuhkan disiplin pada client.

Tim juga mengukur ukuran respons. Daftar komentar awal memakai:

TS
.select('*')

Query diperbaiki menjadi:

TS
.select('id, task_id, author_id, body, created_at')

Perbedaannya belum besar karena tabel masih sederhana. Namun, pemilihan kolom eksplisit mencegah kolom internal baru ikut terkirim tanpa sengaja.

Untuk menjaga metrik tetap dapat dipercaya, setiap pengujian mencatat:

  • Versi migration

  • Commit aplikasi

  • Jumlah record

  • Region proyek

  • Waktu pengujian

  • Identitas role

  • Query yang dipakai

  • Hasil query plan

Checklist sebelum memperluas Supabase ke fitur lain

Tim memakai pemeriksaan berikut untuk kandidat migrasi berikutnya:

  • Fitur memiliki baseline waktu dan error.

  • Entitas serta hubungannya sudah dipetakan.

  • Data sensitif sudah diklasifikasikan.

  • Setiap tabel memiliki primary key.

  • Foreign key memiliki perilaku penghapusan yang jelas.

  • Constraint melindungi aturan penting.

  • RLS aktif pada schema yang terekspos.

  • Policy tersedia untuk setiap operasi.

  • Policy diuji dengan minimal dua akun.

  • Pengguna anonim diuji.

  • Pergantian role dan pencabutan akses diuji.

  • Query utama memiliki index.

  • Realtime memiliki alasan yang terukur.

  • Subscription dibersihkan saat tidak dipakai.

  • service_role hanya tersedia pada server tepercaya.

  • Migration berhasil dari database kosong.

  • Tipe frontend dibuat ulang.

  • Staging memakai volume data yang realistis.

  • Backup dan prosedur restore tersedia.

  • Metrik keberhasilan disepakati sebelum implementasi.

Kesimpulan

Studi kasus ini menunjukkan bahwa Supabase Database dapat memangkas pekerjaan backend standar tanpa mengubah PostgreSQL menjadi kotak misterius. Waktu implementasi turun, endpoint khusus berkurang, dan aturan akses menjadi lebih dekat dengan data yang dilindungi.

Hasil tersebut datang dari schema yang rapi, RLS yang diuji, migration berversi, query terukur, serta pemakaian Realtime yang selektif. Supabase menyediakan jalan pintas untuk boilerplate, bukan izin untuk melewati desain database dan keamanan.

Pelajari dokumentasi resmi Supabase Database, lalu pilih satu alur kecil dengan baseline yang jelas. Migrasikan, ukur, dan perluas hanya ketika hasilnya benar-benar lebih baik—bukan sekadar karena demonya terlihat mulus.


Referensi

Supabase. (2026). Supabase: The Postgres Development Platform.

Supabase. (2026). Database.

GitHub. (2026). Supabase/Supabase: The Postgres Development Platform.

Refine. (2026). Create Your Own Supabase Database in 5 Minutes.

MindStudio. (2026). What Is Supabase? The Open-Source Firebase Alternative Explained.

Blog. (2026). Everything You Need to Know About Supabase: From Beginner to Expert.

GitHub. (2026). Supabase.

SAP AI. (2026). What Is Supabase?

Suparbase. (2026). Docs.

Steve Ronald. (2026). How to Create a Supabase Project for Beginners: 2026 Step-by-Step Guide.

Komentar (0)

Belum ada komentar. Jadilah yang pertama berbagi pendapat!

Tinggalkan komentar