Programming
Tutorial Supabase: Bangun Backend JavaScript
Daftar isi
- Latar Belakang: Kenapa Backend Sering Menghambat Pengembangan?
- Tantangan yang Perlu Diselesaikan
- Pendekatan Solusi dengan Supabase
- Supabase Dibandingkan dengan Firebase dan PostgreSQL Mandiri
- Pilih Supabase Jika
- Pilih Firebase Jika
- Pilih PostgreSQL Mandiri Jika
- Fitur Utama Supabase yang Perlu Dipahami
- PostgreSQL Database
- API Otomatis
- Supabase Auth
- Supabase Storage
- Supabase Realtime
- Edge Functions
- Vector dan pgvector
- Persiapan Sebelum Mulai
- Model Data yang Akan Dibuat
- Step 1: Buat Proyek Supabase
- Kenapa Pemilihan Wilayah Penting?
- Data yang Perlu Dicatat
- Step 2: Buat Proyek JavaScript Lokal
- Kenapa Variabel Lingkungan Dibutuhkan?
- Kenapa Klien Dibuat dalam Berkas Terpisah?
- Kesalahan yang Sering Terjadi
- Step 3: Buat Tabel Tasks dengan SQL
- Kenapa Memakai UUID?
- Kenapa Memakai Foreign Key?
- Kenapa Memakai Constraint Judul?
- Step 4: Tambahkan Trigger Updated At
- Kenapa Memakai Trigger?
- Uji Trigger
- Step 5: Aktifkan Row Level Security
- Kenapa Langkah Ini Wajib?
- Buat Policy untuk Membaca Tugas
- Buat Policy untuk Menambah Tugas
- Buat Policy untuk Mengubah Tugas
- Buat Policy untuk Menghapus Tugas
- Kenapa UPDATE Memakai USING dan WITH CHECK?
- Kesalahan yang Sering Terjadi: Data Kosong Padahal Sudah Ada
- Step 6: Buat Registrasi Pengguna
- Kenapa Verifikasi Email Penting?
- Validasi Formulir Registrasi
- Kesalahan yang Sering Terjadi
- Step 7: Buat Login dan Logout
- Dengarkan Perubahan Sesi
- Kenapa Perubahan Sesi Perlu Didengarkan?
- Step 8: Tambahkan Tugas ke Database
- Kenapa Memanggil .select() Setelah Insert?
- Kesalahan RLS Saat Insert
- Step 9: Ambil dan Urutkan Daftar Tugas
- Kenapa Tidak Memakai .select('') di Semua Tempat?
- Pagination untuk Data Besar
- Step 10: Ubah Status Tugas
- Kenapa Filter .eq('id', taskId) Penting?
- Apa yang Terjadi Jika ID Milik Pengguna Lain?
- Step 11: Hapus Tugas dengan Aman
- Tambahkan Konfirmasi di Antarmuka
- Step 12: Buat Bucket Storage untuk Lampiran
- Kenapa Bucket Private Lebih Aman?
- Rancang Struktur Path
- Buat Policy Upload
- Buat Policy Read
- Buat Policy Delete
- Step 13: Unggah Berkas ke Supabase Storage
- Kenapa Nama Berkas Dibersihkan?
- Kenapa upsert Diatur false?
- Validasi Ukuran dan Tipe Berkas
- Step 14: Buat Signed URL untuk Membuka Lampiran
- Kenapa Signed URL Punya Batas Waktu?
- Step 15: Aktifkan Pembaruan Realtime
- Kenapa Subscription Harus Dibersihkan?
- Perlukah Mengambil Ulang Semua Data?
- Step 16: Buat Antarmuka HTML Sederhana
- Render Daftar Tugas dengan Aman
- Kenapa Memakai textContent, Bukan innerHTML?
- Step 17: Hubungkan Form dengan Supabase
- Tangani Perubahan Checkbox
- Kenapa Tombol atau Input Dinonaktifkan Saat Permintaan Berjalan?
- Step 18: Tambahkan Penanganan Error yang Konsisten
- Kenapa Pesan Teknis Tidak Selalu Cocok Ditampilkan?
- Kode Error PostgreSQL yang Berguna
- Step 19: Tambahkan Index Berdasarkan Pola Query
- Kenapa Index Tidak Boleh Ditambahkan Sembarangan?
- Step 20: Pindahkan Operasi Sensitif ke Edge Function
- Kenapa Function Tetap Memakai Token Pengguna?
- Step 21: Kelola Skema dengan Migration
- Kenapa Migration Penting?
- Step 22: Siapkan Konfigurasi Lokal, Staging, dan Produksi
- Kenapa Data Produksi Tidak Boleh Disalin Sembarangan?
- Step 23: Uji Keamanan RLS Secara Sengaja
- Uji Anonim
- Kenapa Pengujian Negatif Penting?
- Step 24: Pantau Performa, Log, dan Biaya
- Kenapa Egress Perlu Diperhatikan?
- Connection Pooling
- Studi Kasus: Sistem Tugas untuk Tim Operasional Lokal
- Background
- Challenge atau Problem
- Approach
- Implementation
- Results
- Key Learnings
- Kelebihan Supabase dalam Proyek Nyata
- 1. PostgreSQL Sungguhan
- 2. Waktu Implementasi Lebih Singkat
- 3. RLS Menyatukan Data dan Otorisasi
- 4. Ekosistem Open Source
- 5. Cocok untuk Banyak Jenis Aplikasi
- Kekurangan dan Batasan Supabase
- 1. Tetap Membutuhkan Pemahaman Database
- 2. RLS Punya Kurva Belajar
- 3. Realtime Bukan Solusi untuk Semua Data
- 4. Biaya Perlu Dipantau Saat Skala Naik
- 5. Self-Hosting Menambah Tanggung Jawab
- Siapa yang Paling Cocok Memakai Supabase?
- Rekomendasi Berdasarkan Skenario
- Kesalahan Umum Saat Menggunakan Supabase
- Menaruh Service Role Key di Frontend
- Mengandalkan Filter Frontend untuk Keamanan
- Mengaktifkan RLS Tanpa Policy
- Membuat Policy Terlalu Luas
- Memakai .single() Saat Hasil Bisa Kosong
- Mengambil Semua Data Sekaligus
- Tidak Menyimpan Skema dalam Migration
- Menganggap Realtime Selalu Lebih Baik
- Troubleshooting Supabase
- Error: Failed to Fetch
- Error: Invalid API Key
- Error: Relation Does Not Exist
- Error: Permission Denied
- Realtime Tidak Menerima Event
- Upload Storage Ditolak
- Query Terasa Lambat
- Tips Praktis Supabase untuk Proyek Produksi
- Mulai dari Aturan Akses
- Gunakan Constraint untuk Aturan yang Tidak Boleh Dilanggar
- Gunakan Transaksi untuk Operasi yang Saling Bergantung
- Pisahkan Data Publik dan Privat
- Catat Aktivitas Penting
- Batasi Data dari Awal
- Uji Pemulihan, Bukan Hanya Backup
- Checklist Sebelum Rilis
- Database
- Keamanan
- Autentikasi
- Performa dan Biaya
- Operasional
- Langkah Pengembangan Berikutnya
- Kesimpulan
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:
-
Pengguna A tidak boleh membaca data milik pengguna B.
-
Kunci dengan hak administratif tidak boleh masuk ke kode frontend.
-
Berkas pribadi tidak boleh dibuka melalui URL publik sembarangan.
-
Koneksi database perlu tetap stabil saat jumlah pengguna bertambah.
-
Perubahan skema harus bisa ditelusuri dan diterapkan ulang.
-
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:
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

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.
Baca juga OpenCode Free: Agen AI Coding Gratis dan Open Source
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:
node --version
npm --version
Contoh keluaran:
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 |
|---|---|---|
|
| Identitas unik tugas |
|
| Pemilik tugas |
|
| Judul tugas |
|
| Status penyelesaian |
|
| Lokasi berkas di Storage |
|
| Waktu pembuatan |
|
| 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:
-
Pilih organisasi.
-
Masukkan nama proyek, misalnya
task-manager. -
Buat kata sandi database yang kuat.
-
Pilih wilayah server yang dekat dengan mayoritas pengguna.
-
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:
Baca juga ClinePass: Langganan Model Open Weight untuk Coding
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:
mkdir supabase-task-manager
cd supabase-task-manager
npm init -y
npm install @supabase/supabase-js
Keluaran yang diharapkan:
added packages, and audited packages
found 0 vulnerabilities
Buat struktur berkas:
supabase-task-manager/
├── index.html
├── app.js
├── supabase.js
├── styles.css
├── .env
├── .gitignore
└── package.json
Tambahkan .env ke .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:
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:
npm create vite@latest . -- --template vanilla
npm install
npm install @supabase/supabase-js
Lalu buat klien Supabase di src/supabase.js:
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:
Invalid supabaseUrl
Penyebab umum:
-
Nilai URL belum dimasukkan.
-
Nama variabel tidak diawali
VITE_. -
Server pengembangan belum dimulai ulang setelah
.envdiubah. -
URL tidak memakai format
https://...supabase.co.
Perbaikan:
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:
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:
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:
Baca juga Kiro Dev: IDE Agentic AWS untuk Spec-Driven Development
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:
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:
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:
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:
select
trigger_name,
event_manipulation,
event_object_table
from information_schema.triggers
where event_object_table = 'tasks';
Keluaran yang diharapkan:
tasks_set_updated_at | UPDATE | tasks
Step 5: Aktifkan Row Level Security
Aktifkan RLS pada tabel:
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
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
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
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
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:
-
usingmenentukan baris yang boleh dipilih untuk diubah. -
with checkmemeriksa 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:
data: []
error: null
Kemungkinan besar query berhasil, tetapi policy tidak mengizinkan baris apa pun.
Periksa:
Baca juga Codex CLI dari OpenAI: Panduan Lengkap Ngoding di Terminal
-
Pengguna sudah login.
-
user_idpada baris sesuaiauth.uid(). -
Policy dibuat untuk role
authenticated. -
Sesi belum kedaluwarsa.
-
Data tidak dibuat dengan
user_idyang salah.
Untuk melihat identitas pengguna aktif dari aplikasi:
const {
data: { user },
error
} = await supabase.auth.getUser()
console.log({ user, error })
Step 6: Buat Registrasi Pengguna
Tambahkan fungsi registrasi:
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:
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:
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:
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:
User already registered
Artinya alamat email sudah pernah digunakan. Arahkan pengguna ke halaman login atau pemulihan kata sandi.
Pesan:
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:
export async function signIn(email, password) {
const { data, error } =
await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
throw error
}
return data
}
Pemakaian:
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:
Login sebagai: [email protected]
Sesi berakhir pada: 178...
Fungsi logout:
export async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) {
throw error
}
}
Dengarkan Perubahan Sesi
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:
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.
Baca juga CodeBuddy: Editor Kode AI untuk Coding Lebih Cepat
Step 8: Tambahkan Tugas ke Database
Buat fungsi berikut:
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:
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:
{
"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:
new row violates row-level security policy
Periksa apakah:
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:
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:
try {
const tasks = await getTasks()
console.table(tasks)
} catch (error) {
console.error('Gagal mengambil tugas:', error.message)
}
Keluaran yang diharapkan:
┌─────────┬──────────────────────────────┬─────────────┐
│ 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:
const { data, error } = await supabase
.from('tasks')
.select('id, title, is_complete, created_at')
.order('created_at', { ascending: false })
.range(0, 19)
Halaman kedua:
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:
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:
const updatedTask = await updateTaskStatus(
'7df8c6b8-6e5a-4cb0-a9a2-7d54e443ec48',
true
)
console.log(updatedTask)
Keluaran yang diharapkan:
{
"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.
Baca juga GitHub Copilot: Cara Mempercepat Kerja Tim
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:
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:
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:
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
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:
alter table public.tasks
add column deleted_at timestamptz;
Lalu tandai sebagai terhapus:
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:
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:
{user_id}/{task_id}/{nama_berkas}
Contoh:
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
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
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
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:
Baca juga Mengenal Astro 7.1: Framework JavaScript Ringan untuk Website
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:
-
Unggah versi baru.
-
Ubah referensi database.
-
Pastikan perubahan berhasil.
-
Hapus versi lama bila aturan retensi mengizinkan.
Validasi Ukuran dan Tipe Berkas
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:
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:
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:
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:
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:
const channel = subscribeToTasks(async () => {
const tasks = await getTasks()
renderTasks(tasks)
})
Bersihkan channel saat halaman tidak lagi membutuhkannya:
await supabase.removeChannel(channel)
Keluaran yang diharapkan:
Status realtime: SUBSCRIBED
Saat data berubah:
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:
Baca juga 9Router v0.5.35: Solusi Rate Limit AI Coding
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:
<!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
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:
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:
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
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:
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 |
|---|---|---|
| Nilai unik sudah ada | Minta nilai lain |
| Foreign key tidak valid | Periksa data terkait |
| Kolom wajib kosong | Periksa payload |
| Melanggar check constraint | Periksa aturan nilai |
| Izin ditolak | Periksa RLS dan role |
| Hasil tidak cocok dengan | Periksa jumlah baris |
Step 19: Tambahkan Index Berdasarkan Pola Query
Query aplikasi mengambil tugas berdasarkan pengguna dan mengurutkannya menurut waktu pembuatan. Buat index:
create index tasks_user_created_at_idx
on public.tasks (user_id, created_at desc);
Jika sering menyaring tugas berdasarkan status:
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:
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:
Baca juga Cursor Composer 3: AI Coding 1,5T Parameter Segera Rilis
npm install --save-dev supabase
Login dan hubungkan proyek:
npx supabase login
npx supabase link --project-ref PROJECT_REF
Buat function:
npx supabase functions new task-summary
Isi supabase/functions/task-summary/index.ts:
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:
npx supabase functions deploy task-summary
Panggil dari aplikasi:
const { data, error } =
await supabase.functions.invoke('task-summary')
if (error) {
throw error
}
console.log(data)
Keluaran:
{
"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:
npx supabase init
npx supabase start
Buat migration:
npx supabase migration new create_tasks
Isi berkas SQL yang dibuat dengan definisi tabel, trigger, index, dan policy.
Terapkan perubahan lokal:
npx supabase db reset
Kirim migration ke proyek terhubung:
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.development
VITE_SUPABASE_URL=https://proyek-dev.supabase.co
VITE_SUPABASE_ANON_KEY=kunci-dev
# .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:
Baca juga GPT Image 2: Panduan Lengkap API Gambar AI OpenAI
-
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:
-
Login sebagai akun A.
-
Buat sebuah tugas.
-
Catat ID tugas.
-
Logout.
-
Login sebagai akun B.
-
Coba membaca tugas akun A.
-
Coba mengubah tugas akun A.
-
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:
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('id', taskIdMilikAkunA)
console.log({ data, error })
Keluaran yang diharapkan untuk akun B:
{
"data": [],
"error": null
}
Uji Anonim
Jalankan query tanpa login:
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:
-
Membuat proyek pengembangan.
-
Mendesain tabel pengguna, tugas, dan area kerja.
-
Menambahkan foreign key dan constraint.
-
Mengaktifkan RLS sebelum frontend terhubung.
-
Menguji akses menggunakan dua akun.
-
Membuat form tugas dan unggah lampiran.
-
Menambahkan realtime pada papan operasional.
-
Menyimpan skema dalam migration.
-
Menyiapkan staging.
-
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:
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:
.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:
-
Buat tabel.
-
Aktifkan RLS.
-
Buat policy.
-
Uji dengan akun pemilik.
-
Uji dengan akun lain.
-
Baru hubungkan fitur lengkap.
Membuat Policy Terlalu Luas
Hindari policy seperti:
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:
.maybeSingle()
Contoh:
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:
-
Buka URL proyek di browser.
-
Periksa tab Network.
-
Pastikan
.envterbaca. -
Mulai ulang server lokal.
-
Periksa status proyek.
-
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.
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:
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()sesuaiuser_id. -
Function memakai
security invokeratausecurity definersesuai 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.objectstersedia. -
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:
-
Pilih kolom yang dibutuhkan.
-
Tambahkan filter lebih awal.
-
Gunakan pagination.
-
Periksa index.
-
Jalankan
EXPLAIN ANALYZE. -
Hindari pola N+1.
-
Periksa ukuran payload.
-
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:
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, danDELETEsesuai 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
profilesyang terhubung keauth.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
pgvectorbila 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