Programming

Tutorial Supabase: Bangun Backend JavaScript

M
MUGHU
43 menit baca
Tutorial Supabase: Bangun Backend JavaScript
Daftar isi

Supabase cocok buat Teman-Teman yang ingin membangun backend aplikasi tanpa harus merakit database, autentikasi, penyimpanan berkas, API, dan fitur realtime dari nol. Platform ini memakai PostgreSQL sebagai fondasi, lalu menambahkan layanan terintegrasi yang bisa dipakai lewat dashboard, SQL, atau pustaka klien.

Supabase adalah platform backend berbasis PostgreSQL yang menyediakan database, autentikasi, API otomatis, penyimpanan berkas, realtime, Edge Functions, dan dukungan vector embedding dalam satu ekosistem.

Tutorial ini membangun aplikasi pencatat tugas sederhana dengan Supabase dan JavaScript. Hasil akhirnya mencakup registrasi pengguna, operasi tambah-baca-ubah-hapus, keamanan berbasis Row Level Security, unggah berkas, pembaruan realtime, dan persiapan menuju lingkungan produksi.

Ringkasan singkat: Supabase menawarkan jalur cepat untuk membangun backend aplikasi web, mobile, dan AI. Kekuatan utamanya ada pada PostgreSQL, integrasi antarlayanan, serta kebebasan untuk memakai SQL standar atau melakukan self-hosting bila dibutuhkan.

Latar Belakang: Kenapa Backend Sering Menghambat Pengembangan?

Membuat antarmuka aplikasi biasanya terasa cepat. Begitu masuk ke backend, daftar pekerjaannya mulai panjang:

  • Menyediakan server database.

  • Mendesain tabel dan relasi.

  • Membuat endpoint API.

  • Menangani registrasi dan login.

  • Menyimpan foto atau dokumen.

  • Mengatur izin akses setiap pengguna.

  • Menyiapkan pembaruan realtime.

  • Mengelola koneksi, pencadangan, log, dan deployment.

Untuk aplikasi kecil, pekerjaan tersebut bisa memakan waktu lebih lama daripada fitur utama yang sebenarnya ingin dibuat. Bagi bisnis lokal di Jakarta, Bandung, Surabaya, Yogyakarta, atau kota lain di Indonesia, waktu tambahan ini punya konsekuensi nyata: biaya pengembangan bertambah dan validasi pasar tertunda.

Supabase mencoba menyelesaikan masalah itu dengan pendekatan backend as a service. Teman-Teman tetap mendapat database PostgreSQL sungguhan, tetapi pekerjaan operasional yang berulang sudah disiapkan.

Tantangan yang Perlu Diselesaikan

Misalkan sebuah tim sedang membangun aplikasi pengelolaan pesanan untuk usaha kuliner di Bandung. Setiap anggota perlu login, melihat pesanan yang menjadi tanggung jawabnya, mengunggah bukti transaksi, dan menerima perubahan status tanpa memuat ulang halaman.

Secara teknis, kebutuhannya terlihat sederhana. Namun, ada beberapa risiko:

  1. Pengguna A tidak boleh membaca data milik pengguna B.

  2. Kunci dengan hak administratif tidak boleh masuk ke kode frontend.

  3. Berkas pribadi tidak boleh dibuka melalui URL publik sembarangan.

  4. Koneksi database perlu tetap stabil saat jumlah pengguna bertambah.

  5. Perubahan skema harus bisa ditelusuri dan diterapkan ulang.

  6. Biaya harus tetap masuk akal selama tahap validasi.

Masalah terbesar bukan sekadar menyimpan data. Tantangannya adalah menyimpan dan membuka data kepada pihak yang tepat, melalui jalur yang aman, dengan biaya operasional yang terkendali.

Pendekatan Solusi dengan Supabase

Untuk tutorial ini, arsitektur aplikasinya dibuat seperti berikut:

TEXT
Browser
   |
   | supabase-js + JWT pengguna
   v
Supabase API
   |
   +-- Auth
   +-- PostgreSQL + Row Level Security
   +-- Storage
   +-- Realtime
   +-- Edge Functions

Browser memakai supabase-js untuk berkomunikasi dengan layanan Supabase. Setelah login, Supabase Auth memberikan sesi berbasis JWT.

PostgreSQL kemudian memeriksa JWT tersebut melalui Row Level Security atau RLS. Jadi, keamanan tidak hanya bergantung pada filter yang ditulis di frontend.

RLS adalah mekanisme PostgreSQL yang membatasi baris mana yang boleh dibaca atau diubah oleh pengguna berdasarkan kebijakan di tingkat database.

Model ini penting karena kode antarmuka bisa saja salah. Pengguna juga dapat mencoba memanggil API secara langsung. Dengan RLS, aturan akses tetap diperiksa oleh database.

Supabase Dibandingkan dengan Firebase dan PostgreSQL Mandiri

PostgreSQL Mandiri

Supabase sering disebut sebagai alternatif open-source untuk Firebase. Perbandingan itu membantu, tetapi keduanya memakai model data dan pendekatan arsitektur yang berbeda.

Kriteria

Supabase

Firebase

PostgreSQL Mandiri

Database utama

PostgreSQL relasional

Firestore berbasis dokumen

PostgreSQL relasional

Query kompleks

Kuat dengan SQL dan JOIN

Perlu menyesuaikan model dokumen

Sangat kuat

Autentikasi

Terintegrasi

Terintegrasi

Perlu disiapkan sendiri

API otomatis

REST dan GraphQL dari skema

SDK dan API Firebase

Perlu dibuat atau memakai alat tambahan

Realtime

Postgres Changes, Broadcast, Presence

Fitur inti yang matang

Perlu dirakit sendiri

Penyimpanan berkas

Terintegrasi dengan kebijakan akses

Terintegrasi

Perlu layanan tambahan

Self-hosting

Tersedia

Tidak untuk seluruh platform

Tersedia sepenuhnya

Beban operasional

Rendah hingga sedang

Rendah

Sedang hingga tinggi

Portabilitas data

Tinggi karena PostgreSQL

Lebih terikat pada model Firebase

Sangat tinggi

Cocok untuk

Aplikasi relasional dan tim yang suka SQL

Aplikasi yang cocok dengan model dokumen

Tim yang perlu kendali infrastruktur penuh

Pilih Supabase Jika

  • Data punya relasi yang jelas, seperti pengguna, pesanan, produk, dan pembayaran.

  • Tim nyaman memakai SQL atau ingin mempelajarinya secara bertahap.

  • Aplikasi membutuhkan autentikasi, storage, dan realtime dalam satu platform.

  • Portabilitas database menjadi pertimbangan jangka panjang.

  • Tim ingin bergerak cepat tanpa kehilangan akses ke kemampuan PostgreSQL.

Pilih Firebase Jika

  • Model data dokumen lebih sesuai dengan kebutuhan aplikasi.

  • Tim sudah memakai ekosistem Google secara mendalam.

  • Fitur sinkronisasi klien Firebase memenuhi pola aplikasi dengan baik.

  • Tim tidak membutuhkan query relasional dan JOIN yang kompleks.

Pilih PostgreSQL Mandiri Jika

  • Organisasi punya tim operasi yang siap mengelola database.

  • Ada aturan infrastruktur atau keamanan yang sangat khusus.

  • Kendali penuh lebih penting daripada kecepatan pengembangan.

  • Aplikasi tidak membutuhkan layanan backend terintegrasi.

Rekomendasi praktisnya cukup sederhana. Untuk aplikasi bisnis dengan data relasional, Supabase sering menjadi titik tengah yang menarik: lebih cepat daripada merakit backend sendiri, tetapi lebih dekat ke teknologi database standar daripada platform yang memakai abstraksi tertutup.

Fitur Utama Supabase yang Perlu Dipahami

Sebelum masuk ke kode, ada baiknya memahami bagian-bagian yang akan dipakai.

PostgreSQL Database

Setiap proyek Supabase menyediakan database PostgreSQL. Teman-Teman bisa memakai:

  • Tabel dan relasi.

  • Foreign key.

  • Constraint.

  • Index.

  • View dan materialized view.

  • Function dan trigger.

  • Tipe jsonb.

  • Full-text search.

  • Ekstensi seperti pgvector.

Referensi lengkap mengenai perilaku SQL dan tipe data bisa dipelajari lewat dokumentasi PostgreSQL.

API Otomatis

Skema database otomatis tersedia melalui API. Artinya, setelah membuat tabel dan kebijakan akses, frontend bisa langsung menjalankan operasi data tanpa menulis endpoint CRUD satu per satu.

Kemudahan ini bukan berarti setiap logika harus diletakkan di browser. Operasi rahasia, integrasi pembayaran, dan penggunaan kunci administratif tetap perlu dijalankan di server atau Edge Functions.

Supabase Auth

Supabase Auth menangani:

  • Email dan kata sandi.

  • Magic link.

  • One-time password.

  • Penyedia OAuth.

  • Pengelolaan sesi.

  • Multi-factor authentication pada skenario yang didukung.

  • Integrasi identitas pengguna dengan RLS.

Supabase Storage

Storage menyimpan gambar, video, PDF, dan berkas lain dalam bucket. Aksesnya dapat diatur memakai kebijakan, mirip dengan pengamanan tabel.

Supabase Realtime

Realtime punya tiga kemampuan utama:

  • Postgres Changes untuk mendengarkan perubahan tabel.

  • Broadcast untuk mengirim pesan singkat antarklien.

  • Presence untuk melacak status pengguna yang sedang terhubung.

Edge Functions

Edge Functions menjalankan kode TypeScript atau JavaScript di sisi server. Fitur ini tepat untuk:

  • Menerima webhook pembayaran.

  • Memanggil layanan eksternal.

  • Mengirim email.

  • Memvalidasi operasi sensitif.

  • Menjaga secret agar tidak masuk ke browser.

Vector dan pgvector

Ekstensi pgvector memungkinkan vector embedding disimpan bersama data relasional. Ini berguna untuk pencarian semantik, rekomendasi, dan retrieval-augmented generation tanpa langsung menambah database vektor terpisah.

Persiapan Sebelum Mulai

Tutorial ini memakai JavaScript biasa agar alurnya mudah dilihat. Konsep yang sama bisa diterapkan pada React, Next.js, Vue, Svelte, React Native, Flutter, Swift, atau Python.

Siapkan beberapa hal berikut:

  • Akun Supabase.

  • Node.js versi LTS.

  • npm.

  • Penyunting kode.

  • Browser modern.

  • Pemahaman ringan tentang JavaScript dan SQL.

  • Git, bila proyek akan disimpan dalam repositori.

Periksa instalasi Node.js dan npm:

BASH
node --version
npm --version

Contoh keluaran:

TEXT
v22.17.0
10.9.2

Versi di perangkat Teman-Teman tidak harus sama persis. Yang penting, gunakan versi Node.js yang masih didukung.

Model Data yang Akan Dibuat

Aplikasi akan memakai tabel tasks dengan struktur berikut:

Kolom

Tipe

Fungsi

id

uuid

Identitas unik tugas

user_id

uuid

Pemilik tugas

title

text

Judul tugas

is_complete

boolean

Status penyelesaian

attachment_path

text

Lokasi berkas di Storage

created_at

timestamptz

Waktu pembuatan

updated_at

timestamptz

Waktu perubahan terakhir

Relasi user_id mengarah ke pengguna pada skema autentikasi. Setiap tugas hanya boleh diakses oleh pemiliknya.

Step 1: Buat Proyek Supabase

Buka dashboard resmi Supabase, lalu buat proyek baru.

Isi pengaturan berikut:

  1. Pilih organisasi.

  2. Masukkan nama proyek, misalnya task-manager.

  3. Buat kata sandi database yang kuat.

  4. Pilih wilayah server yang dekat dengan mayoritas pengguna.

  5. Tunggu hingga proyek selesai disiapkan.

Kenapa Pemilihan Wilayah Penting?

Jarak jaringan memengaruhi latensi. Jika sebagian besar pengguna berada di Indonesia, pilih wilayah Asia yang tersedia dan paling sesuai dengan kebutuhan proyek.

Lokasi database juga dapat berkaitan dengan kebijakan penyimpanan data organisasi. Untuk aplikasi yang menangani data sensitif, keputusan wilayah sebaiknya melibatkan tim keamanan dan kepatuhan, bukan hanya tim pengembang.

Data yang Perlu Dicatat

Setelah proyek aktif, buka bagian pengaturan API dan temukan:

  • Project URL.

  • Publishable key atau anon key untuk klien.

  • Service role key untuk server.

Jangan menaruh service role key di browser, repositori publik, atau aplikasi mobile. Kunci tersebut memiliki hak tinggi dan dapat melewati RLS pada konteks tertentu.

Contoh nilai yang aman dipakai di frontend:

TEXT
Project URL:
https://contoh-proyek.supabase.co

Publishable/anon key:
eyJhbGciOi...

Kunci publik memang dirancang untuk digunakan oleh aplikasi klien. Keamanan data tetap harus ditegakkan melalui RLS dan kebijakan Storage.

Step 2: Buat Proyek JavaScript Lokal

Buat folder aplikasi dan pasang pustaka Supabase:

BASH
mkdir supabase-task-manager
cd supabase-task-manager
npm init -y
npm install @supabase/supabase-js

Keluaran yang diharapkan:

TEXT
added packages, and audited packages
found 0 vulnerabilities

Buat struktur berkas:

TEXT
supabase-task-manager/
├── index.html
├── app.js
├── supabase.js
├── styles.css
├── .env
├── .gitignore
└── package.json

Tambahkan .env ke .gitignore:

GITIGNORE
node_modules
.env
.env.local

Kenapa Variabel Lingkungan Dibutuhkan?

URL proyek dan kunci publik sebaiknya tidak ditulis berulang kali di banyak berkas. Variabel lingkungan membuat konfigurasi lebih mudah dipisahkan antara lokal, staging, dan produksi.

Walaupun anon key bukan secret administratif, menyimpan konfigurasi dalam .env tetap membantu mencegah kekacauan saat deployment.

Untuk contoh berbasis Vite, ubah nama variabel menjadi:

ENV
VITE_SUPABASE_URL=https://contoh-proyek.supabase.co
VITE_SUPABASE_ANON_KEY=masukkan_kunci_publik

Buat proyek Vite bila Teman-Teman ingin menjalankan contoh secara langsung:

BASH
npm create vite@latest . -- --template vanilla
npm install
npm install @supabase/supabase-js

Lalu buat klien Supabase di src/supabase.js:

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

const supabaseUrl = import.meta.env. VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env. VITE_SUPABASE_ANON_KEY

if (!supabaseUrl || !supabaseAnonKey) {
  throw new Error('Konfigurasi Supabase belum tersedia.')
}

export const supabase = createClient(
  supabaseUrl,
  supabaseAnonKey
)

Kenapa Klien Dibuat dalam Berkas Terpisah?

Satu instance klien bisa dipakai untuk database, Auth, Storage, Realtime, dan Edge Functions. Berkas terpisah menghindari pembuatan koneksi yang tidak perlu dan membuat konfigurasi lebih mudah diuji.

Kesalahan yang Sering Terjadi

Pesan kesalahan:

TEXT
Invalid supabaseUrl

Penyebab umum:

  • Nilai URL belum dimasukkan.

  • Nama variabel tidak diawali VITE_.

  • Server pengembangan belum dimulai ulang setelah .env diubah.

  • URL tidak memakai format https://...supabase.co.

Perbaikan:

BASH
npm run dev

Hentikan dan jalankan ulang perintah tersebut setelah mengubah .env.

Step 3: Buat Tabel Tasks dengan SQL

Buka SQL Editor di dashboard Supabase, lalu jalankan:

SQL
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),
  is_complete boolean not null default false,
  attachment_path text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

Keluaran yang diharapkan:

TEXT
Success. No rows returned

Kenapa Memakai UUID?

UUID cocok untuk data yang dibuat dari banyak klien karena nilainya tidak bergantung pada nomor urut global. UUID juga tidak membocorkan jumlah data melalui pola ID seperti 1, 2, dan 3.

UUID bukan pengganti kebijakan keamanan. Pengguna tetap tidak boleh diberi akses hanya karena ID sulit ditebak.

Kenapa Memakai Foreign Key?

Bagian berikut menghubungkan tugas dengan pengguna:

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

Foreign key menjaga integritas data. Nilai user_id harus merujuk ke pengguna yang memang ada.

on delete cascade berarti tugas ikut dihapus saat akun pengguna dihapus. Perilaku ini perlu disesuaikan dengan kebutuhan bisnis. Untuk sistem audit atau keuangan, penghapusan otomatis mungkin bukan pilihan yang tepat.

Kenapa Memakai Constraint Judul?

Constraint berikut menjaga judul antara 1 dan 200 karakter:

SQL
check (char_length(title) between 1 and 200)

Validasi frontend membantu pengalaman pengguna. Constraint database menjaga aturan tetap berlaku walaupun data masuk dari skrip, API lain, atau kesalahan aplikasi.

Step 4: Tambahkan Trigger Updated At

Kolom updated_at sebaiknya berubah otomatis setiap kali tugas diperbarui.

Jalankan SQL berikut:

SQL
create or replace function public.set_updated_at()
returns trigger
language plpgsql
security invoker
set search_path = ''
as $$
begin
  new.updated_at = now();
  return new;
end;
$$;

Buat trigger:

SQL
create trigger tasks_set_updated_at
before update on public.tasks
for each row
execute function public.set_updated_at();

Kenapa Memakai Trigger?

Tanpa trigger, setiap klien harus mengingat untuk mengubah updated_at. Itu mudah terlupakan dan hasilnya tidak konsisten.

Dengan trigger, database menjadi sumber aturan. Semua jalur perubahan data mendapat perilaku yang sama.

Uji Trigger

Masukkan data sementara melalui SQL hanya jika sudah ada pengguna yang valid. Cara yang lebih praktis adalah menguji setelah fitur autentikasi selesai.

Untuk memeriksa trigger yang terpasang:

SQL
select
  trigger_name,
  event_manipulation,
  event_object_table
from information_schema.triggers
where event_object_table = 'tasks';

Keluaran yang diharapkan:

TEXT
tasks_set_updated_at | UPDATE | tasks

Step 5: Aktifkan Row Level Security

Aktifkan RLS pada tabel:

SQL
alter table public.tasks enable row level security;

Kenapa Langkah Ini Wajib?

Tanpa RLS yang tepat, tabel pada skema yang diekspos API dapat berisiko diakses lebih luas daripada yang dimaksud. RLS mengubah aturan bisnis menjadi pengamanan di tingkat database.

Setelah RLS aktif dan belum ada policy, permintaan melalui klien publik biasanya tidak dapat membaca atau mengubah baris. Ini perilaku yang aman: akses ditutup sampai ada aturan yang jelas.

Buat Policy untuk Membaca Tugas

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

Buat Policy untuk Menambah Tugas

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

Buat Policy untuk Mengubah Tugas

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

Buat Policy untuk Menghapus Tugas

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

Kenapa UPDATE Memakai USING dan WITH CHECK?

Pada operasi UPDATE:

  • using menentukan baris yang boleh dipilih untuk diubah.

  • with check memeriksa hasil baru setelah perubahan.

Keduanya mencegah pengguna mengambil baris sendiri lalu mengganti user_id menjadi identitas orang lain.

Kesalahan yang Sering Terjadi: Data Kosong Padahal Sudah Ada

Gejalanya:

JAVASCRIPT
data: []
error: null

Kemungkinan besar query berhasil, tetapi policy tidak mengizinkan baris apa pun.

Periksa:

  • Pengguna sudah login.

  • user_id pada baris sesuai auth.uid().

  • Policy dibuat untuk role authenticated.

  • Sesi belum kedaluwarsa.

  • Data tidak dibuat dengan user_id yang salah.

Untuk melihat identitas pengguna aktif dari aplikasi:

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

console.log({ user, error })

Step 6: Buat Registrasi Pengguna

Tambahkan fungsi registrasi:

JAVASCRIPT
import { supabase } from './supabase.js'

export async function signUp(email, password) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  })

  if (error) {
    throw error
  }

  return data
}

Panggil fungsi tersebut:

JAVASCRIPT
try {
  const result = await signUp(
    '[email protected]',
    'KataSandi-Yang-Kuat-2026'
  )

  console.log('Registrasi berhasil:', result)
} catch (error) {
  console.error('Registrasi gagal:', error.message)
}

Keluaran yang mungkin muncul:

TEXT
Registrasi berhasil:
{
  user: { ... },
  session: null
}

session: null dapat terjadi jika konfirmasi email diaktifkan. Pengguna perlu membuka tautan konfirmasi sebelum login.

Kenapa Verifikasi Email Penting?

Verifikasi mengurangi akun palsu dan membantu memastikan alamat email benar-benar dimiliki pengguna. Namun, pengiriman email bawaan biasanya punya batas penggunaan.

Untuk aplikasi produksi, pertimbangkan SMTP khusus agar:

  • Reputasi pengiriman lebih mudah dikelola.

  • Nama pengirim sesuai merek.

  • Batas kirim lebih cocok dengan jumlah pengguna.

  • Log pengiriman lebih mudah diperiksa.

Validasi Formulir Registrasi

Validasi ringan di frontend:

JAVASCRIPT
function validateRegistration(email, password) {
  const errors = []

  if (!email.includes('@')) {
    errors.push('Alamat email tidak valid.')
  }

  if (password.length < 12) {
    errors.push('Kata sandi minimal 12 karakter.')
  }

  return errors
}

Validasi ini membantu pengguna, tetapi jangan menjadikannya satu-satunya pertahanan. Aturan keamanan harus tetap diterapkan pada layanan autentikasi dan server.

Kesalahan yang Sering Terjadi

Pesan:

TEXT
User already registered

Artinya alamat email sudah pernah digunakan. Arahkan pengguna ke halaman login atau pemulihan kata sandi.

Pesan:

TEXT
Email rate limit exceeded

Terlalu banyak email dikirim dalam waktu singkat. Tunggu beberapa saat, periksa batas pengiriman, dan gunakan SMTP yang sesuai untuk produksi.

Step 7: Buat Login dan Logout

Fungsi login:

JAVASCRIPT
export async function signIn(email, password) {
  const { data, error } =
    await supabase.auth.signInWithPassword({
      email,
      password,
    })

  if (error) {
    throw error
  }

  return data
}

Pemakaian:

JAVASCRIPT
try {
  const { user, session } = await signIn(
    '[email protected]',
    'KataSandi-Yang-Kuat-2026'
  )

  console.log('Login sebagai:', user.email)
  console.log('Sesi berakhir pada:', session.expires_at)
} catch (error) {
  console.error(error.message)
}

Keluaran yang diharapkan:

TEXT
Login sebagai: [email protected]
Sesi berakhir pada: 178...

Fungsi logout:

JAVASCRIPT
export async function signOut() {
  const { error } = await supabase.auth.signOut()

  if (error) {
    throw error
  }
}

Dengarkan Perubahan Sesi

JAVASCRIPT
supabase.auth.onAuthStateChange((event, session) => {
  console.log('Auth event:', event)

  if (session) {
    console.log('Pengguna aktif:', session.user.email)
  } else {
    console.log('Tidak ada sesi aktif.')
  }
})

Event yang mungkin terlihat:

TEXT
INITIAL_SESSION
SIGNED_IN
TOKEN_REFRESHED
SIGNED_OUT

Kenapa Perubahan Sesi Perlu Didengarkan?

Sesi dapat berubah karena login, logout, pemulihan token, atau aktivitas di tab lain. Antarmuka perlu merespons perubahan tersebut agar tidak menampilkan data yang sudah tidak boleh diakses.

Hindari melakukan pekerjaan berat langsung di callback perubahan sesi. Simpan status secukupnya, lalu perbarui antarmuka melalui alur aplikasi.

Step 8: Tambahkan Tugas ke Database

Buat fungsi berikut:

JAVASCRIPT
export async function createTask(title) {
  const {
    data: { user },
    error: userError,
  } = await supabase.auth.getUser()

  if (userError) {
    throw userError
  }

  if (!user) {
    throw new Error('Silakan login terlebih dahulu.')
  }

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

  if (error) {
    throw error
  }

  return data
}

Panggil fungsi:

JAVASCRIPT
try {
  const task = await createTask(
    'Menyiapkan laporan penjualan mingguan'
  )

  console.log('Tugas baru:', task)
} catch (error) {
  console.error('Gagal menambah tugas:', error.message)
}

Keluaran yang diharapkan:

JSON
{
  "id": "7df8c6b8-6e5a-4cb0-a9a2-7d54e443ec48",
  "user_id": "91ecb1c8-89b6-4e83-9817-55ac2fdb1310",
  "title": "Menyiapkan laporan penjualan mingguan",
  "is_complete": false,
  "attachment_path": null,
  "created_at": "2026-07-12T00:30:00.000Z",
  "updated_at": "2026-07-12T00:30:00.000Z"
}

Kenapa Memanggil .select() Setelah Insert?

Operasi insert tidak selalu mengembalikan baris lengkap kecuali diminta. .select() meminta data yang baru dibuat, termasuk ID dan nilai default dari database.

.single() menyatakan bahwa aplikasi mengharapkan tepat satu baris. Ini membuat bentuk hasil lebih mudah dipakai.

Kesalahan RLS Saat Insert

Pesan yang umum:

TEXT
new row violates row-level security policy

Periksa apakah:

JAVASCRIPT
user_id: user.id

benar-benar memakai ID pengguna aktif. Jangan menerima user_id bebas dari formulir.

Step 9: Ambil dan Urutkan Daftar Tugas

Buat fungsi:

JAVASCRIPT
export async function getTasks() {
  const { data, error } = await supabase
    .from('tasks')
    .select(`
      id,
      title,
      is_complete,
      attachment_path,
      created_at,
      updated_at
    `)
    .order('created_at', { ascending: false })

  if (error) {
    throw error
  }

  return data
}

Pemakaian:

JAVASCRIPT
try {
  const tasks = await getTasks()
  console.table(tasks)
} catch (error) {
  console.error('Gagal mengambil tugas:', error.message)
}

Keluaran yang diharapkan:

TEXT
┌─────────┬──────────────────────────────┬─────────────┐
│ id      │ title                        │ is_complete │
├─────────┼──────────────────────────────┼─────────────┤
│ ...     │ Menyiapkan laporan mingguan │ false       │
└─────────┴──────────────────────────────┴─────────────┘

Kenapa Tidak Memakai .select('*') di Semua Tempat?

Meminta semua kolom memang praktis saat eksplorasi. Untuk aplikasi produksi, memilih kolom yang dibutuhkan punya beberapa manfaat:

  • Payload lebih kecil.

  • Kontrak data lebih jelas.

  • Kolom internal tidak ikut terkirim.

  • Perubahan skema lebih mudah dikendalikan.

RLS tetap menjadi pengaman utama. Pemilihan kolom bukan pengganti kebijakan akses.

Pagination untuk Data Besar

Untuk mengambil 20 data pertama:

JAVASCRIPT
const { data, error } = await supabase
  .from('tasks')
  .select('id, title, is_complete, created_at')
  .order('created_at', { ascending: false })
  .range(0, 19)

Halaman kedua:

JAVASCRIPT
const { data, error } = await supabase
  .from('tasks')
  .select('id, title, is_complete, created_at')
  .order('created_at', { ascending: false })
  .range(20, 39)

Pagination mencegah aplikasi mengunduh ribuan baris sekaligus. Ini menurunkan waktu respons, penggunaan memori, dan biaya transfer data.

Step 10: Ubah Status Tugas

Buat fungsi:

JAVASCRIPT
export async function updateTaskStatus(taskId, isComplete) {
  const { data, error } = await supabase
    .from('tasks')
    .update({
      is_complete: isComplete,
    })
    .eq('id', taskId)
    .select()
    .single()

  if (error) {
    throw error
  }

  return data
}

Pemakaian:

JAVASCRIPT
const updatedTask = await updateTaskStatus(
  '7df8c6b8-6e5a-4cb0-a9a2-7d54e443ec48',
  true
)

console.log(updatedTask)

Keluaran yang diharapkan:

JSON
{
  "title": "Menyiapkan laporan penjualan mingguan",
  "is_complete": true,
  "updated_at": "2026-07-12T00:42:13.128Z"
}

Nilai updated_at berubah otomatis karena trigger yang dibuat sebelumnya.

Kenapa Filter .eq('id', taskId) Penting?

Tanpa filter, operasi update dapat menargetkan lebih banyak baris daripada yang dimaksud. Pustaka atau konfigurasi mungkin memberi perlindungan tertentu, tetapi jangan mengandalkannya sebagai kebiasaan.

Untuk operasi perubahan data, biasakan memeriksa:

  • Tabel tujuan.

  • Nilai perubahan.

  • Filter.

  • Policy.

  • Bentuk data yang dikembalikan.

Apa yang Terjadi Jika ID Milik Pengguna Lain?

RLS mencegah pengguna memperbarui baris tersebut. Biasanya tidak ada baris yang dikembalikan atau operasi gagal sesuai kondisi permintaan.

Aplikasi sebaiknya menampilkan pesan netral:

TEXT
Tugas tidak ditemukan atau tidak dapat diubah.

Pesan ini lebih aman daripada membocorkan bahwa suatu ID memang ada tetapi dimiliki orang lain.

Step 11: Hapus Tugas dengan Aman

Buat fungsi:

JAVASCRIPT
export async function deleteTask(taskId) {
  const { data, error } = await supabase
    .from('tasks')
    .delete()
    .eq('id', taskId)
    .select('id')
    .single()

  if (error) {
    throw error
  }

  return data
}

Pemakaian:

JAVASCRIPT
try {
  const deleted = await deleteTask(
    '7df8c6b8-6e5a-4cb0-a9a2-7d54e443ec48'
  )

  console.log('Tugas dihapus:', deleted.id)
} catch (error) {
  console.error('Gagal menghapus:', error.message)
}

Tambahkan Konfirmasi di Antarmuka

JAVASCRIPT
const confirmed = window.confirm(
  'Hapus tugas ini? Tindakan ini tidak dapat dibatalkan.'
)

if (confirmed) {
  await deleteTask(taskId)
}

Untuk data bisnis penting, pertimbangkan soft delete daripada penghapusan permanen.

Tambahkan kolom:

SQL
alter table public.tasks
add column deleted_at timestamptz;

Lalu tandai sebagai terhapus:

JAVASCRIPT
await supabase
  .from('tasks')
  .update({ deleted_at: new Date().toISOString() })
  .eq('id', taskId)

Soft delete mendukung pemulihan data dan audit, tetapi query serta policy menjadi lebih kompleks. Gunakan hanya jika kebutuhan bisnis memang membenarkannya.

Step 12: Buat Bucket Storage untuk Lampiran

Buka menu Storage dan buat bucket bernama:

TEXT
task-attachments

Pilih bucket private agar berkas tidak dapat dibuka secara publik tanpa pemeriksaan akses.

Kenapa Bucket Private Lebih Aman?

Lampiran tugas bisa berisi invoice, foto identitas, laporan, atau dokumen internal. Bucket publik memberi URL yang dapat diakses siapa pun yang memilikinya.

Bucket private mengharuskan aplikasi memakai sesi pengguna dan kebijakan akses. Jika berkas perlu dibagikan sementara, gunakan signed URL dengan masa berlaku terbatas.

Rancang Struktur Path

Gunakan pola:

TEXT
{user_id}/{task_id}/{nama_berkas}

Contoh:

TEXT
91ecb1c8-89b6-4e83-9817-55ac2fdb1310/
7df8c6b8-6e5a-4cb0-a9a2-7d54e443ec48/
invoice-juli.pdf

Path yang diawali ID pengguna memudahkan policy memeriksa kepemilikan.

Buat Policy Upload

SQL
create policy "Users can upload task attachments"
on storage.objects
for insert
to authenticated
with check (
  bucket_id = 'task-attachments'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);

Buat Policy Read

SQL
create policy "Users can read their task attachments"
on storage.objects
for select
to authenticated
using (
  bucket_id = 'task-attachments'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);

Buat Policy Delete

SQL
create policy "Users can delete their task attachments"
on storage.objects
for delete
to authenticated
using (
  bucket_id = 'task-attachments'
  and (storage.foldername(name))[1] = (select auth.uid())::text
);

Policy path ini memastikan folder pertama sama dengan ID pengguna aktif. Untuk sistem dengan akses tim, struktur dan policy perlu diperluas berdasarkan organisasi atau keanggotaan proyek.

Step 13: Unggah Berkas ke Supabase Storage

Buat fungsi:

JAVASCRIPT
export async function uploadTaskAttachment(taskId, file) {
  const {
    data: { user },
    error: userError,
  } = await supabase.auth.getUser()

  if (userError) {
    throw userError
  }

  if (!user) {
    throw new Error('Silakan login terlebih dahulu.')
  }

  const safeName = file.name
    .toLowerCase()
    .replace(/[^a-z0-9._-]/g, '-')

  const filePath = `${user.id}/${taskId}/${crypto.randomUUID()}-${safeName}`

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

  if (error) {
    throw error
  }

  const { error: updateError } = await supabase
    .from('tasks')
    .update({
      attachment_path: data.path,
    })
    .eq('id', taskId)

  if (updateError) {
    await supabase.storage
      .from('task-attachments')
      .remove([data.path])

    throw updateError
  }

  return data
}

Kenapa Nama Berkas Dibersihkan?

Nama berkas dari perangkat pengguna dapat memuat spasi, simbol, atau karakter yang menyulitkan URL dan logging. Membersihkannya membuat path lebih konsisten.

Penambahan crypto.randomUUID() mencegah benturan ketika dua berkas punya nama sama.

Kenapa upsert Diatur false?

upsert: false mencegah berkas lama tertimpa diam-diam. Untuk dokumen bisnis, penggantian tanpa jejak bisa menjadi masalah audit.

Jika aplikasi memang perlu mengganti berkas, lakukan alur yang jelas:

  1. Unggah versi baru.

  2. Ubah referensi database.

  3. Pastikan perubahan berhasil.

  4. Hapus versi lama bila aturan retensi mengizinkan.

Validasi Ukuran dan Tipe Berkas

JAVASCRIPT
function validateAttachment(file) {
  const allowedTypes = [
    'application/pdf',
    'image/jpeg',
    'image/png',
  ]

  const maxSize = 5 * 1024 * 1024

  if (!allowedTypes.includes(file.type)) {
    throw new Error('Format berkas tidak didukung.')
  }

  if (file.size > maxSize) {
    throw new Error('Ukuran berkas maksimal 5 MB.')
  }
}

Jalankan sebelum upload:

JAVASCRIPT
validateAttachment(file)
await uploadTaskAttachment(taskId, file)

Validasi sisi klien meningkatkan pengalaman pengguna, tetapi pemeriksaan sensitif sebaiknya juga dilakukan di server. Nilai file.type dari browser tidak selalu dapat dipercaya sepenuhnya.

Step 14: Buat Signed URL untuk Membuka Lampiran

Karena bucket bersifat private, gunakan signed URL:

JAVASCRIPT
export async function getAttachmentUrl(path) {
  const { data, error } = await supabase.storage
    .from('task-attachments')
    .createSignedUrl(path, 300)

  if (error) {
    throw error
  }

  return data.signedUrl
}

Pemakaian:

JAVASCRIPT
const url = await getAttachmentUrl(task.attachment_path)
window.open(url, '_blank', 'noopener,noreferrer')

Angka 300 berarti URL berlaku selama 300 detik atau 5 menit.

Kenapa Signed URL Punya Batas Waktu?

URL sementara mengurangi dampak jika tautan tersalin ke tempat yang tidak semestinya. Setelah masa berlaku habis, akses perlu dibuat ulang melalui pengguna yang masih berwenang.

Untuk dokumen sangat sensitif, masa berlaku dapat dibuat lebih pendek. Teman-Teman juga perlu mempertimbangkan caching browser dan kemungkinan pengguna mengunduh berkas sebelum URL kedaluwarsa.

Step 15: Aktifkan Pembaruan Realtime

Supabase Realtime dapat mengirim perubahan tabel melalui WebSocket. Pastikan tabel tasks masuk ke publication realtime sesuai pengaturan proyek.

Kemudian buat subscription:

JAVASCRIPT
export function subscribeToTasks(onChange) {
  const channel = supabase
    .channel('tasks-changes')
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'tasks',
      },
      (payload) => {
        console.log('Perubahan tugas:', payload)
        onChange(payload)
      }
    )
    .subscribe((status) => {
      console.log('Status realtime:', status)
    })

  return channel
}

Pemakaian:

JAVASCRIPT
const channel = subscribeToTasks(async () => {
  const tasks = await getTasks()
  renderTasks(tasks)
})

Bersihkan channel saat halaman tidak lagi membutuhkannya:

JAVASCRIPT
await supabase.removeChannel(channel)

Keluaran yang diharapkan:

TEXT
Status realtime: SUBSCRIBED

Saat data berubah:

TEXT
Perubahan tugas:
{
  eventType: "UPDATE",
  new: { ... },
  old: { ... }
}

Kenapa Subscription Harus Dibersihkan?

Jika komponen atau halaman membuat subscription berulang tanpa menghapus yang lama, satu perubahan bisa diproses berkali-kali. Dampaknya:

  • Render ganda.

  • Koneksi bertambah.

  • Penggunaan memori naik.

  • Debugging menjadi membingungkan.

Perlukah Mengambil Ulang Semua Data?

Untuk aplikasi kecil, mengambil ulang daftar setelah event realtime adalah pendekatan sederhana dan konsisten.

Untuk data besar, perbarui state berdasarkan payload:

JAVASCRIPT
function applyRealtimeEvent(tasks, payload) {
  if (payload.eventType === 'INSERT') {
    return [payload.new, ...tasks]
  }

  if (payload.eventType === 'UPDATE') {
    return tasks.map((task) =>
      task.id === payload.new.id ? payload.new : task
    )
  }

  if (payload.eventType === 'DELETE') {
    return tasks.filter(
      (task) => task.id !== payload.old.id
    )
  }

  return tasks
}

Pendekatan ini lebih hemat permintaan, tetapi memerlukan pengelolaan state yang rapi.

Step 16: Buat Antarmuka HTML Sederhana

Contoh struktur halaman:

HTML
<!doctype html>
<html lang="id">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0"
    />
    <title>Supabase Task Manager</title>
  </head>
  <body>
    <main>
      <section id="auth-section">
        <h2>Masuk</h2>

        <form id="login-form">
          <label>
            Email
            <input
              id="email"
              type="email"
              autocomplete="email"
              required
            />
          </label>

          <label>
            Kata sandi
            <input
              id="password"
              type="password"
              autocomplete="current-password"
              minlength="12"
              required
            />
          </label>

          <button type="submit">Masuk</button>
        </form>
      </section>

      <section id="task-section" hidden>
        <div>
          <span id="user-email"></span>
          <button id="logout-button">Keluar</button>
        </div>

        <form id="task-form">
          <label>
            Tugas baru
            <input
              id="task-title"
              maxlength="200"
              required
            />
          </label>

          <button type="submit">Tambah</button>
        </form>

        <p id="status-message" role="status"></p>
        <ul id="task-list"></ul>
      </section>
    </main>

    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Render Daftar Tugas dengan Aman

JAVASCRIPT
function renderTasks(tasks) {
  const taskList = document.querySelector('#task-list')
  taskList.replaceChildren()

  for (const task of tasks) {
    const item = document.createElement('li')
    const title = document.createElement('span')
    const checkbox = document.createElement('input')

    checkbox.type = 'checkbox'
    checkbox.checked = task.is_complete
    checkbox.dataset.taskId = task.id

    title.textContent = task.title

    item.append(checkbox, title)
    taskList.append(item)
  }
}

Kenapa Memakai textContent, Bukan innerHTML?

Judul tugas berasal dari input pengguna. Memasukkannya melalui innerHTML bisa membuka celah cross-site scripting jika input tidak ditangani dengan benar.

textContent memperlakukan nilai sebagai teks biasa, bukan markup HTML.

Hindari pola berikut:

JAVASCRIPT
item.innerHTML = `<span>${task.title}</span>`

Pola tersebut terlihat ringkas, tetapi berbahaya bila task.title berisi markup atau skrip.

Step 17: Hubungkan Form dengan Supabase

Contoh alur utama:

JAVASCRIPT
import { supabase } from './supabase.js'
import {
  signIn,
  signOut,
  createTask,
  getTasks,
  updateTaskStatus,
  subscribeToTasks,
} from './api.js'

const authSection = document.querySelector('#auth-section')
const taskSection = document.querySelector('#task-section')
const loginForm = document.querySelector('#login-form')
const taskForm = document.querySelector('#task-form')
const logoutButton = document.querySelector('#logout-button')
const statusMessage =
  document.querySelector('#status-message')

let realtimeChannel = null

async function showTasks() {
  const tasks = await getTasks()
  renderTasks(tasks)
}

async function handleSession(session) {
  const isLoggedIn = Boolean(session)

  authSection.hidden = isLoggedIn
  taskSection.hidden = !isLoggedIn

  if (!isLoggedIn) {
    if (realtimeChannel) {
      await supabase.removeChannel(realtimeChannel)
      realtimeChannel = null
    }

    return
  }

  document.querySelector('#user-email').textContent =
    session.user.email

  await showTasks()

  if (!realtimeChannel) {
    realtimeChannel = subscribeToTasks(showTasks)
  }
}

loginForm.addEventListener('submit', async (event) => {
  event.preventDefault()
  statusMessage.textContent = ''

  const email = document.querySelector('#email').value
  const password =
    document.querySelector('#password').value

  try {
    await signIn(email, password)
    loginForm.reset()
  } catch (error) {
    statusMessage.textContent = error.message
  }
})

taskForm.addEventListener('submit', async (event) => {
  event.preventDefault()

  const input = document.querySelector('#task-title')
  const title = input.value.trim()

  if (!title) return

  try {
    await createTask(title)
    input.value = ''
  } catch (error) {
    statusMessage.textContent = error.message
  }
})

logoutButton.addEventListener('click', async () => {
  try {
    await signOut()
  } catch (error) {
    statusMessage.textContent = error.message
  }
})

supabase.auth.onAuthStateChange((event, session) => {
  queueMicrotask(() => handleSession(session))
})

Tangani Perubahan Checkbox

JAVASCRIPT
document
  .querySelector('#task-list')
  .addEventListener('change', async (event) => {
    const target = event.target

    if (
      !(target instanceof HTMLInputElement) ||
      target.type !== 'checkbox'
    ) {
      return
    }

    target.disabled = true

    try {
      await updateTaskStatus(
        target.dataset.taskId,
        target.checked
      )
    } catch (error) {
      target.checked = !target.checked
      statusMessage.textContent = error.message
    } finally {
      target.disabled = false
    }
  })

Kenapa Tombol atau Input Dinonaktifkan Saat Permintaan Berjalan?

Pengguna bisa mengeklik dua kali ketika jaringan lambat. Menonaktifkan kontrol sementara membantu mencegah permintaan ganda dan state yang saling bertabrakan.

Untuk operasi pembuatan pesanan atau pembayaran, perlindungan di antarmuka saja belum cukup. Gunakan idempotency key atau pemeriksaan transaksi di server.

Step 18: Tambahkan Penanganan Error yang Konsisten

Supabase biasanya mengembalikan objek data dan error. Jangan mengabaikan error, meskipun aplikasi terlihat berjalan pada kondisi normal.

Buat pembungkus:

JAVASCRIPT
export async function executeSupabase(request) {
  const { data, error } = await request

  if (error) {
    console.error({
      message: error.message,
      code: error.code,
      details: error.details,
      hint: error.hint,
    })

    throw new Error(mapErrorMessage(error))
  }

  return data
}

function mapErrorMessage(error) {
  if (error.code === '23505') {
    return 'Data tersebut sudah tersedia.'
  }

  if (error.code === '23503') {
    return 'Data terkait tidak ditemukan.'
  }

  if (error.code === '42501') {
    return 'Teman-Teman tidak memiliki izin untuk tindakan ini.'
  }

  return 'Terjadi kendala. Silakan coba lagi.'
}

Kenapa Pesan Teknis Tidak Selalu Cocok Ditampilkan?

Pesan database bisa berisi nama tabel, constraint, atau detail internal. Informasi tersebut membantu pengembang, tetapi dapat membingungkan pengguna dan membuka terlalu banyak informasi sistem.

Pisahkan:

  • Pesan ramah untuk pengguna.

  • Detail teknis untuk log.

  • ID pelacakan untuk menghubungkan laporan pengguna dengan log.

Kode Error PostgreSQL yang Berguna

Kode

Arti umum

Tindakan

23505

Nilai unik sudah ada

Minta nilai lain

23503

Foreign key tidak valid

Periksa data terkait

23502

Kolom wajib kosong

Periksa payload

23514

Melanggar check constraint

Periksa aturan nilai

42501

Izin ditolak

Periksa RLS dan role

PGRST116

Hasil tidak cocok dengan .single()

Periksa jumlah baris

Step 19: Tambahkan Index Berdasarkan Pola Query

Query aplikasi mengambil tugas berdasarkan pengguna dan mengurutkannya menurut waktu pembuatan. Buat index:

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

Jika sering menyaring tugas berdasarkan status:

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

Kenapa Index Tidak Boleh Ditambahkan Sembarangan?

Index mempercepat pembacaan tertentu, tetapi punya biaya:

  • Memakai ruang penyimpanan.

  • Menambah pekerjaan saat insert dan update.

  • Perlu dirawat oleh database.

  • Tidak otomatis membantu semua query.

Tambahkan index berdasarkan pola query nyata. Gunakan EXPLAIN ANALYZE untuk melihat rencana eksekusi:

SQL
explain analyze
select id, title, is_complete, created_at
from public.tasks
where user_id = 'UUID-PENGGUNA'
order by created_at desc
limit 20;

Hasil yang baik bergantung pada jumlah data dan distribusinya. Pada tabel yang masih berisi beberapa baris, PostgreSQL bisa memilih sequential scan karena memang lebih murah.

Step 20: Pindahkan Operasi Sensitif ke Edge Function

Operasi CRUD biasa dapat berjalan langsung dari browser selama RLS benar. Namun, beberapa tindakan harus berada di server:

  • Memakai service role key.

  • Memanggil API pembayaran.

  • Mengirim email dengan secret.

  • Membuat laporan lintas pengguna.

  • Menjalankan logika harga.

  • Memproses webhook.

Pasang Supabase CLI:

BASH
npm install --save-dev supabase

Login dan hubungkan proyek:

BASH
npx supabase login
npx supabase link --project-ref PROJECT_REF

Buat function:

BASH
npx supabase functions new task-summary

Isi supabase/functions/task-summary/index.ts:

TYPESCRIPT
import { createClient } from 'npm:@supabase/supabase-js@2'

Deno.serve(async (request) => {
  const authorization =
    request.headers.get('Authorization')

  if (!authorization) {
    return new Response(
      JSON.stringify({ error: 'Unauthorized' }),
      {
        status: 401,
        headers: {
          'Content-Type': 'application/json',
        },
      }
    )
  }

  const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_ANON_KEY')!,
    {
      global: {
        headers: {
          Authorization: authorization,
        },
      },
    }
  )

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

  if (userError || !user) {
    return new Response(
      JSON.stringify({ error: 'Invalid session' }),
      {
        status: 401,
        headers: {
          'Content-Type': 'application/json',
        },
      }
    )
  }

  const { count, error } = await supabase
    .from('tasks')
    .select('*', {
      count: 'exact',
      head: true,
    })
    .eq('is_complete', false)

  if (error) {
    return new Response(
      JSON.stringify({ error: error.message }),
      {
        status: 400,
        headers: {
          'Content-Type': 'application/json',
        },
      }
    )
  }

  return new Response(
    JSON.stringify({
      user_id: user.id,
      incomplete_tasks: count ?? 0,
    }),
    {
      headers: {
        'Content-Type': 'application/json',
      },
    }
  )
})

Deploy:

BASH
npx supabase functions deploy task-summary

Panggil dari aplikasi:

JAVASCRIPT
const { data, error } =
  await supabase.functions.invoke('task-summary')

if (error) {
  throw error
}

console.log(data)

Keluaran:

JSON
{
  "user_id": "91ecb1c8-89b6-4e83-9817-55ac2fdb1310",
  "incomplete_tasks": 4
}

Kenapa Function Tetap Memakai Token Pengguna?

Klien Supabase di function meneruskan token pengguna. Akibatnya, query tetap mengikuti RLS pengguna tersebut.

Ini sering lebih aman daripada langsung memakai service role. Gunakan service role hanya saat operasi memang membutuhkan hak administratif, lalu lakukan pemeriksaan otorisasi sendiri dengan ketat.

Step 21: Kelola Skema dengan Migration

Mengubah tabel hanya melalui dashboard nyaman untuk eksplorasi. Untuk kerja tim dan deployment, skema perlu disimpan sebagai migration.

Inisialisasi lingkungan lokal:

BASH
npx supabase init
npx supabase start

Buat migration:

BASH
npx supabase migration new create_tasks

Isi berkas SQL yang dibuat dengan definisi tabel, trigger, index, dan policy.

Terapkan perubahan lokal:

BASH
npx supabase db reset

Kirim migration ke proyek terhubung:

BASH
npx supabase db push

Kenapa Migration Penting?

Migration memberi beberapa keuntungan:

  • Perubahan skema masuk ke version control.

  • Anggota tim mendapat struktur database yang sama.

  • Lingkungan staging dan produksi bisa diselaraskan.

  • Riwayat perubahan lebih mudah diaudit.

  • Pemulihan lingkungan tidak bergantung pada ingatan seseorang.

Kesalahan yang sering MUGHU lihat dalam proyek backend bukan SQL yang rumit, melainkan perubahan kecil di dashboard yang tidak pernah dicatat. Beberapa minggu kemudian, lingkungan lokal, staging, dan produksi punya struktur berbeda.

Step 22: Siapkan Konfigurasi Lokal, Staging, dan Produksi

Gunakan proyek terpisah untuk setiap lingkungan penting:

Lingkungan

Tujuan

Jenis data

Lokal

Pengembangan harian

Data contoh

Staging

Uji integrasi dan penerimaan

Data uji realistis

Produksi

Pengguna sebenarnya

Data operasional

Jangan memakai database produksi untuk eksperimen. Kesalahan query atau policy dapat memengaruhi pengguna sungguhan.

Contoh variabel lingkungan:

ENV
# .env.development
VITE_SUPABASE_URL=https://proyek-dev.supabase.co
VITE_SUPABASE_ANON_KEY=kunci-dev
ENV
# .env.production
VITE_SUPABASE_URL=https://proyek-prod.supabase.co
VITE_SUPABASE_ANON_KEY=kunci-prod

Kenapa Data Produksi Tidak Boleh Disalin Sembarangan?

Data produksi dapat memuat informasi pribadi. Bila data perlu dipakai untuk pengujian:

  • Hapus atau samarkan identitas.

  • Batasi jumlah data.

  • Catat siapa yang punya akses.

  • Terapkan kebijakan retensi.

  • Hindari menyalin kredensial dan token.

  • Sesuaikan dengan kewajiban perlindungan data organisasi.

Step 23: Uji Keamanan RLS Secara Sengaja

RLS tidak cukup hanya dibuat. Policy perlu diuji dengan skenario negatif.

Buat dua akun:

Lalu lakukan pengujian:

  1. Login sebagai akun A.

  2. Buat sebuah tugas.

  3. Catat ID tugas.

  4. Logout.

  5. Login sebagai akun B.

  6. Coba membaca tugas akun A.

  7. Coba mengubah tugas akun A.

  8. Coba menghapus tugas akun A.

Hasil yang diharapkan:

  • Akun B tidak dapat membaca tugas akun A.

  • Akun B tidak dapat mengubahnya.

  • Akun B tidak dapat menghapusnya.

  • Tidak ada detail sensitif yang bocor melalui pesan error.

Contoh pengujian:

JAVASCRIPT
const { data, error } = await supabase
  .from('tasks')
  .select('*')
  .eq('id', taskIdMilikAkunA)

console.log({ data, error })

Keluaran yang diharapkan untuk akun B:

JSON
{
  "data": [],
  "error": null
}

Uji Anonim

Jalankan query tanpa login:

JAVASCRIPT
await supabase.auth.signOut()

const { data, error } = await supabase
  .from('tasks')
  .select('*')

console.log({ data, error })

Data tugas pribadi tidak boleh terlihat.

Kenapa Pengujian Negatif Penting?

Pengujian biasa hanya membuktikan bahwa pemilik dapat memakai fitur. Pengujian negatif membuktikan bahwa pihak yang tidak berhak benar-benar ditolak.

Untuk aplikasi multi-tenant, uji lintas akun adalah bagian penting sebelum rilis.

Step 24: Pantau Performa, Log, dan Biaya

Backend yang berjalan hari ini belum tentu tetap sehat saat pengguna bertambah. Pantau beberapa indikator berikut:

  • Waktu respons query.

  • Jumlah koneksi database.

  • Error autentikasi.

  • Penggunaan Storage.

  • Egress.

  • Jumlah koneksi Realtime.

  • Pemanggilan Edge Functions.

  • Pertumbuhan ukuran tabel dan index.

  • Query lambat.

  • Tingkat error setelah deployment.

Periksa dokumentasi resmi Supabase dan halaman harga sebelum menetapkan anggaran. Kuota, fitur, serta harga dapat berubah, jadi keputusan produksi sebaiknya memakai informasi terbaru.

Kenapa Egress Perlu Diperhatikan?

Egress adalah data yang keluar dari layanan menuju pengguna atau sistem lain. Aplikasi gambar, video, dan realtime bisa memakai egress lebih cepat daripada aplikasi teks.

Beberapa cara mengendalikannya:

  • Jangan mengambil kolom yang tidak dipakai.

  • Gunakan pagination.

  • Kompres gambar.

  • Gunakan ukuran gambar yang sesuai tampilan.

  • Atur cache.

  • Hindari mengambil ulang data besar untuk setiap event realtime.

  • Simpan hasil agregasi yang mahal bila pola akses membenarkannya.

Connection Pooling

Aplikasi serverless dapat membuka banyak koneksi singkat. Gunakan connection pooler yang disediakan bila pola runtime membutuhkannya.

Untuk koneksi database langsung, pahami perbedaan antara:

  • Direct connection.

  • Session pooling.

  • Transaction pooling.

Beberapa fitur PostgreSQL memiliki perilaku berbeda pada mode pooling tertentu. Pilih berdasarkan driver, runtime, dan pola transaksi aplikasi.

Studi Kasus: Sistem Tugas untuk Tim Operasional Lokal

Background

Sebuah tim operasional beranggotakan delapan orang mengelola pesanan dari beberapa area di Jakarta Selatan. Sebelumnya, pembagian tugas dilakukan melalui percakapan grup dan lembar kerja bersama.

Cara tersebut cukup saat jumlah transaksi masih sedikit. Begitu aktivitas meningkat, pesan penting tenggelam, bukti transaksi tersebar, dan status pekerjaan tidak konsisten.

Challenge atau Problem

Tim menghadapi beberapa masalah:

  • Tidak ada sumber data tunggal.

  • Setiap orang bisa melihat terlalu banyak informasi.

  • Lampiran tersebar di beberapa layanan.

  • Perubahan status tidak langsung terlihat.

  • Rekap mingguan memerlukan pekerjaan manual.

  • Pengembangan backend khusus dinilai terlalu lama untuk tahap validasi.

Target awalnya bukan membangun sistem perusahaan yang rumit. Tim hanya membutuhkan aplikasi internal yang aman, mudah dipakai, dan bisa dikembangkan bila proses bisnis terbukti efektif.

Approach

Arsitektur dipilih dengan beberapa pertimbangan:

  • PostgreSQL untuk data pesanan dan relasi anggota.

  • Supabase Auth untuk login.

  • RLS untuk membatasi data berdasarkan anggota dan area.

  • Storage private untuk bukti transaksi.

  • Realtime untuk perubahan status.

  • Edge Function untuk rekap dan integrasi eksternal.

Tim memulai dari model data kecil. Fitur analitik lanjutan ditunda sampai pola penggunaan mulai terlihat.

Implementation

Implementasi dilakukan bertahap:

  1. Membuat proyek pengembangan.

  2. Mendesain tabel pengguna, tugas, dan area kerja.

  3. Menambahkan foreign key dan constraint.

  4. Mengaktifkan RLS sebelum frontend terhubung.

  5. Menguji akses menggunakan dua akun.

  6. Membuat form tugas dan unggah lampiran.

  7. Menambahkan realtime pada papan operasional.

  8. Menyimpan skema dalam migration.

  9. Menyiapkan staging.

  10. Menjalankan uji penerimaan bersama tim.

Results

Contoh metrik yang masuk akal untuk validasi internal selama beberapa minggu:

Indikator

Sebelum

Setelah

Waktu rekap mingguan

90 menit

15–25 menit

Tugas tanpa status jelas

12–18 per minggu

2–4 per minggu

Waktu menemukan lampiran

3–8 menit

Di bawah 1 menit

Perubahan status terlambat terlihat

Sering

Jarang

Waktu menyiapkan backend awal

Estimasi beberapa minggu

Beberapa hari kerja

Angka tersebut bukan jaminan hasil setiap proyek. Dampaknya bergantung pada kualitas implementasi, disiplin tim, volume transaksi, dan seberapa jelas alur bisnisnya.

Nilai terbesar datang dari pengurangan pekerjaan koordinasi. Tim tidak perlu berpindah-pindah antara percakapan, spreadsheet, dan penyimpanan berkas untuk menyelesaikan satu tugas.

Key Learnings

Beberapa pelajaran yang paling berguna:

  • RLS perlu didesain sejak awal, bukan ditambahkan menjelang rilis.

  • Model data kecil lebih mudah divalidasi daripada skema yang mencoba menampung semua kemungkinan.

  • Realtime sebaiknya dipakai pada informasi yang benar-benar perlu segera berubah.

  • Lampiran private membutuhkan policy dan strategi URL sementara.

  • Migration lebih penting begitu proyek disentuh lebih dari satu orang.

  • Efisiensi backend tidak otomatis memperbaiki proses bisnis yang belum jelas.

Kelebihan Supabase dalam Proyek Nyata

1. PostgreSQL Sungguhan

Teman-Teman mendapat SQL, relasi, foreign key, index, function, trigger, dan ekstensi. Ini penting untuk aplikasi bisnis yang datanya saling terkait.

2. Waktu Implementasi Lebih Singkat

Auth, API, Storage, dan Realtime sudah tersedia. Tim bisa fokus pada alur produk dan pengalaman pengguna.

3. RLS Menyatukan Data dan Otorisasi

Aturan akses dapat ditempatkan dekat dengan data. Ini mengurangi ketergantungan pada filter aplikasi yang mudah terlewat.

4. Ekosistem Open Source

Kode Supabase tersedia secara terbuka melalui organisasi GitHub Supabase. Tim dapat memeriksa komponen, berkontribusi, atau mempertimbangkan self-hosting.

5. Cocok untuk Banyak Jenis Aplikasi

Contohnya:

  • Dashboard internal.

  • Aplikasi pengelolaan pesanan.

  • Sistem reservasi.

  • Portal pelanggan.

  • Aplikasi komunitas.

  • Chat dan notifikasi.

  • Pencarian semantik.

  • Backend aplikasi mobile.

  • Sistem administrasi usaha lokal.

Kekurangan dan Batasan Supabase

1. Tetap Membutuhkan Pemahaman Database

Dashboard memudahkan banyak pekerjaan, tetapi desain tabel, relasi, index, dan transaksi tetap perlu dipahami. Skema yang buruk akan tetap menimbulkan masalah walaupun infrastrukturnya praktis.

2. RLS Punya Kurva Belajar

Policy yang terlalu longgar dapat membocorkan data. Policy yang terlalu ketat membuat aplikasi terlihat rusak.

Pengujian lintas akun dan dokumentasi aturan akses sangat penting.

3. Realtime Bukan Solusi untuk Semua Data

Mengaktifkan realtime pada terlalu banyak tabel bisa meningkatkan kompleksitas dan konsumsi sumber daya. Data laporan bulanan, misalnya, tidak selalu perlu dikirim lewat WebSocket.

4. Biaya Perlu Dipantau Saat Skala Naik

Paket gratis berguna untuk eksperimen dan validasi, tetapi aplikasi produksi perlu memperhitungkan compute, Storage, egress, backup, koneksi realtime, dan layanan tambahan.

5. Self-Hosting Menambah Tanggung Jawab

Self-hosting memberi kendali, tetapi tim harus menangani:

  • Deployment.

  • Pembaruan versi.

  • Backup.

  • Pemulihan.

  • Pemantauan.

  • Keamanan jaringan.

  • Pengiriman email.

  • Skalabilitas komponen.

  • Respons insiden.

Open source tidak berarti biaya operasional menjadi nol.

Siapa yang Paling Cocok Memakai Supabase?

Supabase cocok untuk:

  • Pengembang aplikasi web dan mobile.

  • Startup yang perlu menguji produk dengan cepat.

  • Tim internal yang membutuhkan dashboard operasional.

  • Bisnis Indonesia yang membangun sistem pemesanan, reservasi, atau portal pelanggan.

  • Tim yang lebih nyaman dengan SQL daripada model dokumen.

  • Proyek AI yang ingin menyimpan embedding bersama data relasional.

  • Organisasi yang menghargai portabilitas PostgreSQL.

Supabase mungkin kurang cocok bila:

  • Tim hanya membutuhkan database tanpa Auth, Storage, atau Realtime.

  • Beban kerja membutuhkan arsitektur terdistribusi yang sangat khusus.

  • Organisasi sudah punya platform backend internal yang matang.

  • Model data benar-benar cocok dengan document database dan tidak membutuhkan relasi.

  • Tim tidak siap mengelola SQL serta kebijakan akses.

  • Self-hosting dipilih hanya untuk menghemat biaya tanpa kemampuan operasi yang memadai.

Rekomendasi Berdasarkan Skenario

Skenario

Rekomendasi

MVP aplikasi bisnis dengan data relasional

Supabase sangat layak dipertimbangkan

Aplikasi internal usaha di Indonesia

Cocok, terutama untuk Auth, CRUD, dan Storage

Chat sederhana atau dashboard live

Gunakan Realtime secara terukur

Sistem pembayaran

Pakai Edge Functions untuk secret dan webhook

Aplikasi AI berbasis pencarian semantik

Pertimbangkan PostgreSQL dan pgvector

Sistem dengan compliance khusus

Evaluasi lokasi data, paket, dan self-hosting

Hanya butuh PostgreSQL terkelola

Bandingkan dengan layanan managed PostgreSQL lain

Beban analitik sangat besar

Pertimbangkan gudang data khusus

Berkas video berukuran besar

Hitung Storage, CDN, dan egress lebih dulu

Sistem multi-tenant

Cocok jika RLS dirancang dan diuji dengan ketat

Kesalahan Umum Saat Menggunakan Supabase

Menaruh Service Role Key di Frontend

Ini kesalahan serius. Kunci administratif harus disimpan di lingkungan server.

Cari seluruh repositori:

BASH
grep -R "service_role" .

Jika kunci pernah masuk repositori publik, anggap sudah bocor dan lakukan rotasi.

Mengandalkan Filter Frontend untuk Keamanan

Query seperti berikut bukan pengaman utama:

JAVASCRIPT
.eq('user_id', user.id)

Filter membantu mengambil data yang tepat, tetapi pengguna dapat memodifikasi permintaan. RLS harus tetap mengontrol akses.

Mengaktifkan RLS Tanpa Policy

Hasilnya aplikasi tidak bisa mengakses tabel. Ini aman, tetapi sering membingungkan.

Urutan yang lebih nyaman:

  1. Buat tabel.

  2. Aktifkan RLS.

  3. Buat policy.

  4. Uji dengan akun pemilik.

  5. Uji dengan akun lain.

  6. Baru hubungkan fitur lengkap.

Membuat Policy Terlalu Luas

Hindari policy seperti:

SQL
using (true)

pada data pribadi tanpa alasan yang jelas. Aturan tersebut mengizinkan semua baris untuk role yang dituju.

Memakai .single() Saat Hasil Bisa Kosong

Jika query boleh tidak menemukan baris, gunakan:

JAVASCRIPT
.maybeSingle()

Contoh:

JAVASCRIPT
const { data, error } = await supabase
  .from('tasks')
  .select('*')
  .eq('id', taskId)
  .maybeSingle()

Mengambil Semua Data Sekaligus

Gunakan pagination, filter, dan pemilihan kolom. Jangan menunggu tabel mencapai jutaan baris sebelum memperbaiki pola query.

Tidak Menyimpan Skema dalam Migration

Dashboard mempercepat eksperimen, tetapi perubahan produksi perlu dilacak. Migration membuat skema bisa direproduksi.

Menganggap Realtime Selalu Lebih Baik

Realtime menambah koneksi dan sinkronisasi state. Gunakan hanya ketika keterlambatan beberapa detik memang berdampak pada pengalaman atau proses kerja.

Troubleshooting Supabase

Error: Failed to Fetch

Kemungkinan penyebab:

  • URL proyek salah.

  • Jaringan terputus.

  • Proyek sedang tidak aktif.

  • CORS atau konfigurasi domain bermasalah.

  • Pemblokir jaringan menghalangi permintaan.

  • DNS belum menyelesaikan alamat.

Langkah pemeriksaan:

  1. Buka URL proyek di browser.

  2. Periksa tab Network.

  3. Pastikan .env terbaca.

  4. Mulai ulang server lokal.

  5. Periksa status proyek.

  6. Uji melalui jaringan lain bila perlu.

Error: Invalid API Key

Periksa apakah kunci berasal dari proyek yang sama dengan URL. Jangan mencampur URL staging dengan kunci produksi.

JAVASCRIPT
console.log({
  url: import.meta.env. VITE_SUPABASE_URL,
  hasKey: Boolean(
    import.meta.env. VITE_SUPABASE_ANON_KEY
  ),
})

Jangan mencetak isi kunci lengkap ke log produksi.

Error: Relation Does Not Exist

Contoh:

TEXT
relation "public.tasks" does not exist

Periksa:

  • Migration sudah diterapkan.

  • Nama tabel benar.

  • Skema yang dipakai benar.

  • Aplikasi terhubung ke proyek yang tepat.

  • Huruf besar dan tanda kutip pada nama tabel tidak menimbulkan perbedaan.

Error: Permission Denied

Periksa:

  • RLS aktif atau tidak.

  • Policy tersedia untuk operasinya.

  • Role pengguna adalah authenticated.

  • auth.uid() sesuai user_id.

  • Function memakai security invoker atau security definer sesuai kebutuhan.

  • Grant pada skema dan tabel tidak bermasalah.

Realtime Tidak Menerima Event

Periksa:

  • Tabel sudah diaktifkan untuk Realtime.

  • Channel mencapai status SUBSCRIBED.

  • Policy mengizinkan pengguna membaca baris terkait.

  • Filter subscription benar.

  • Koneksi WebSocket tidak diblokir.

  • Subscription lama sudah dibersihkan.

  • Pengguna masih memiliki sesi aktif.

Upload Storage Ditolak

Periksa:

  • Nama bucket tepat.

  • Policy storage.objects tersedia.

  • Path diawali ID pengguna.

  • Pengguna sudah login.

  • Ukuran berkas tidak melewati batas.

  • MIME type diperbolehkan aplikasi.

  • Nama berkas tidak mengandung karakter bermasalah.

Query Terasa Lambat

Lakukan:

  1. Pilih kolom yang dibutuhkan.

  2. Tambahkan filter lebih awal.

  3. Gunakan pagination.

  4. Periksa index.

  5. Jalankan EXPLAIN ANALYZE.

  6. Hindari pola N+1.

  7. Periksa ukuran payload.

  8. Pantau waktu query dan jaringan secara terpisah.

Query database 30 milidetik masih bisa terasa lambat jika payload besar atau pengguna jauh dari wilayah server.

Tips Praktis Supabase untuk Proyek Produksi

Mulai dari Aturan Akses

Tulis matriks akses sebelum menulis policy:

Peran

Membaca

Menambah

Mengubah

Menghapus

Pemilik tugas

Ya

Ya

Ya

Ya

Anggota tim

Sesuai area

Sesuai area

Sesuai izin

Tidak selalu

Manajer

Semua dalam organisasi

Ya

Ya

Sesuai kebijakan

Pengguna anonim

Tidak

Tidak

Tidak

Tidak

Matriks ini memudahkan pembahasan dengan tim nonteknis.

Gunakan Constraint untuk Aturan yang Tidak Boleh Dilanggar

Contoh:

SQL
alter table public.tasks
add constraint task_title_not_blank
check (char_length(trim(title)) > 0);

Constraint melindungi data dari semua jalur input.

Gunakan Transaksi untuk Operasi yang Saling Bergantung

Jika suatu proses harus mengubah beberapa tabel sekaligus, buat function PostgreSQL atau jalankan transaksi melalui backend. Jangan mengandalkan beberapa permintaan browser terpisah untuk operasi yang harus berhasil atau gagal sebagai satu kesatuan.

Pisahkan Data Publik dan Privat

Jangan meletakkan semua kolom dalam satu tabel hanya karena praktis. Pertimbangkan tabel terpisah untuk:

  • Profil publik.

  • Detail pribadi.

  • Data administrasi.

  • Token integrasi.

  • Catatan audit.

Catat Aktivitas Penting

Untuk operasi sensitif, simpan:

  • Siapa yang bertindak.

  • Tindakan yang dilakukan.

  • Objek yang diubah.

  • Waktu.

  • Nilai penting sebelum dan sesudah perubahan.

  • ID permintaan bila tersedia.

Audit log tidak harus merekam setiap gerakan pengguna. Fokus pada aktivitas yang relevan untuk keamanan dan operasional.

Batasi Data dari Awal

Gunakan:

  • not null.

  • check.

  • Foreign key.

  • Unique constraint.

  • Panjang teks.

  • Daftar status yang jelas.

  • Validasi file.

  • Batas jumlah permintaan.

Data yang terjaga sejak masuk jauh lebih murah daripada dibersihkan setelah bertahun-tahun.

Uji Pemulihan, Bukan Hanya Backup

Backup yang tidak pernah diuji belum membuktikan bahwa data bisa dipulihkan. Dokumentasikan prosedur pemulihan dan lakukan simulasi berkala sesuai tingkat risiko aplikasi.

Checklist Sebelum Rilis

Database

  • Tabel dan relasi sudah ditinjau.

  • Foreign key memiliki perilaku penghapusan yang sesuai.

  • Constraint penting sudah dibuat.

  • Index mengikuti pola query nyata.

  • Migration tersimpan dalam version control.

  • Query utama sudah diuji dengan volume data realistis.

Keamanan

  • RLS aktif pada tabel yang diakses klien.

  • Policy tersedia untuk SELECT, INSERT, UPDATE, dan DELETE sesuai kebutuhan.

  • Pengujian lintas akun sudah dilakukan.

  • Service role key tidak masuk ke frontend.

  • Secret disimpan dalam lingkungan server.

  • Bucket sensitif dibuat private.

  • Signed URL memiliki masa berlaku yang wajar.

Autentikasi

  • Verifikasi email dikonfigurasi.

  • Redirect URL sesuai domain produksi.

  • Alur pemulihan kata sandi diuji.

  • SMTP produksi sudah dinilai.

  • Pesan error tidak membocorkan detail akun.

  • Logout dan refresh sesi sudah diuji.

Performa dan Biaya

  • Pagination sudah diterapkan.

  • Payload query tidak berlebihan.

  • Gambar dan berkas dikompres.

  • Egress diperkirakan.

  • Realtime hanya aktif pada fitur yang membutuhkan.

  • Connection pooling sesuai runtime.

  • Peringatan penggunaan dan anggaran sudah disiapkan.

Operasional

  • Lingkungan lokal, staging, dan produksi dipisahkan.

  • Log error dapat ditelusuri.

  • Prosedur backup dan pemulihan tersedia.

  • Deployment migration punya langkah verifikasi.

  • Prosedur rotasi secret tersedia.

  • Kontak penanggung jawab insiden sudah ditentukan.

Langkah Pengembangan Berikutnya

  • Tambahkan tabel profiles yang terhubung ke auth.users.

  • Buat organisasi dan keanggotaan untuk aplikasi multi-tenant.

  • Tambahkan role anggota, pengelola, dan administrator.

  • Buat pencarian tugas dengan full-text search PostgreSQL.

  • Tambahkan filter status dan rentang tanggal.

  • Terapkan cursor pagination untuk data yang terus bertambah.

  • Tambahkan audit log untuk perubahan sensitif.

  • Buat Edge Function untuk webhook dan integrasi eksternal.

  • Tambahkan pengujian otomatis untuk RLS.

  • Evaluasi pgvector bila aplikasi membutuhkan pencarian semantik.

Kesimpulan

Supabase memberi fondasi backend yang kuat melalui PostgreSQL, autentikasi, penyimpanan berkas, Realtime, dan Edge Functions. Namun, keberhasilan aplikasi tidak ditentukan oleh banyaknya fitur yang digunakan, melainkan oleh ketepatan desain database, penerapan RLS, pengelolaan secret, serta disiplin dalam migration dan pengujian.

Sebelum produksi, pastikan keamanan, performa, biaya, dan pemulihan data diperlakukan sebagai satu kesatuan. Mulailah dari arsitektur sederhana, ukur berdasarkan pola penggunaan nyata, lalu tingkatkan kapasitas secara bertahap. Jadikan dokumentasi resmi Supabase sebagai rujukan utama saat mengimplementasikan atau memverifikasi setiap konfigurasi.

Gunakan checklist rilis sebagai standar kerja, bukan sekadar pemeriksaan terakhir. Bangun fitur berikutnya hanya setelah fondasinya terbukti aman dan dapat dipulihkan—karena aplikasi Supabase yang andal lahir dari keputusan teknis yang konsisten sejak baris pertama hingga deployment produksi.


Referensi

Supabase. (2026). Supabase: The Open Source Firebase Alternative.

Supabase. (2026). Supabase Documentation.

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

Supabase. (2026). Supabase.

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

SAP AI. (2026). What Is Supabase?

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

GitHub. (2026). Supabase.

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

Supabase. (2026). Pricing and Fees.

Komentar (0)

Belum ada komentar. Jadilah yang pertama berbagi pendapat!

Tinggalkan komentar