Ketika SaaS masih kecil, satu proyek Supabase sering cukup untuk autentikasi, Postgres, Storage, Realtime, dan API aplikasi. Saat beban tumbuh, masalah biasanya bukan karena Supabase harus segera ditinggalkan, melainkan karena semua jenis pekerjaan dipaksa berjalan pada jalur yang sama: transaksi pengguna, koneksi Realtime, endpoint singkat, ekspor batch, pemrosesan file, dan pengiriman notifikasi.
Batas Supabase yang sehat adalah: Postgres tetap menjadi sumber data utama, Realtime dipakai hanya untuk event yang benar-benar perlu segera diketahui klien, Edge Functions menangani endpoint dan orkestrasi singkat, sedangkan queue dan worker eksternal menjalankan pekerjaan asinkron yang berat, panjang, atau perlu diisolasi. Pemisahan ini dilakukan berdasarkan sinyal beban dan risiko, bukan semata-mata karena arsitektur terlihat lebih canggih.
Model dasar: pisahkan jalur transaksi, event, dan komputasi
Mulailah dari satu proyek Supabase, tetapi bedakan tanggung jawab setiap komponen sejak awal. Dengan begitu, pemisahan nanti dapat dilakukan tanpa migrasi besar.
- Postgres Supabase: data inti SaaS, transaksi yang konsisten, status pekerjaan, audit trail, dan sumber kebenaran bisnis.
- Supabase Realtime: pembaruan antarmuka yang sensitif terhadap waktu, misalnya status dokumen yang sedang diproses, pesan percakapan, indikator kehadiran, atau perubahan data yang sedang dilihat pengguna.
- Supabase Edge Functions: webhook masuk, endpoint khusus yang tidak cocok langsung diekspos dari tabel, validasi otorisasi tambahan, dan orkestrasi singkat seperti membuat record pekerjaan lalu mengantrekannya.
- Queue dan worker eksternal: pemrosesan file, pengiriman email dalam jumlah besar, sinkronisasi ke API pihak ketiga, ekspor laporan, reindexing, AI inference, retry bertahap, dan pekerjaan yang durasinya tidak dapat diprediksi.
Prinsip praktis: request pengguna seharusnya menyelesaikan validasi dan perubahan data inti secepat mungkin. Pekerjaan yang tidak harus selesai sebelum respons dikembalikan sebaiknya dipindahkan ke jalur asinkron.
Kapan arsitektur satu proyek mulai perlu dipisahkan?
Tidak ada satu angka universal untuk menentukan batas. Gunakan gejala operasional yang dapat diamati, lalu cari jenis beban yang memengaruhi jalur transaksi inti.
1. Koneksi Realtime meningkat atau event terlalu ramai
Realtime cocok saat klien perlu menerima perubahan dengan cepat. Namun, tidak semua perubahan tabel perlu disiarkan ke semua pengguna. Subscription yang terlalu luas dapat meningkatkan jumlah koneksi aktif, volume event, dan kerja otorisasi pada setiap perubahan.
Pemisahan atau pengurangan beban Realtime diperlukan bila Anda melihat gejala berikut:
- Klien berlangganan ke tabel atau channel yang terlalu umum, padahal pengguna hanya membutuhkan subset kecil data.
- Satu perubahan bisnis menghasilkan banyak update database, lalu semuanya dikirim ke banyak klien.
- UI menggunakan Realtime sebagai mekanisme utama untuk memuat data awal, bukan sebagai pelengkap setelah query awal.
- Event operasional seperti log, progress per langkah, atau telemetri dipublikasikan seperti event produk untuk semua pengguna.
Perbaikan pertama bukan selalu menambah infrastruktur. Batasi subscription berdasarkan tenant, workspace, atau resource yang sedang dibuka. Kirim event ringkas, lalu lakukan refetch terarah bila data lengkap diperlukan. Gunakan polling atau refresh manual untuk informasi yang tidak sensitif terhadap waktu.
2. Query analitis mengganggu transaksi inti
Postgres dapat melayani banyak jenis query, tetapi query dashboard, ekspor, agregasi periode panjang, atau pencarian yang belum diindeks dapat bersaing dengan transaksi inti seperti checkout, pembuatan pesanan, atau penyimpanan perubahan pengguna.
Sinyal yang perlu ditindaklanjuti adalah kenaikan latensi endpoint inti ketika dashboard atau ekspor digunakan, koneksi database yang tertahan oleh query panjang, atau penggunaan CPU dan I/O yang dipicu query non-kritis. Sebelum memisahkan sistem, lakukan langkah dasar berikut:
- Periksa query lambat dan rencana eksekusinya.
- Tambahkan atau perbaiki indeks berdasarkan pola filter, join, dan pengurutan yang benar-benar digunakan.
- Batasi rentang waktu, pagination, dan ukuran ekspor.
- Bangun ringkasan atau tabel agregat untuk dashboard yang tidak harus menghitung ulang seluruh data.
- Pindahkan pembuatan file ekspor ke worker, bukan ke request HTTP pengguna.
Jika kebutuhan analitik terus tumbuh, data dapat direplikasi atau diekspor ke sistem analitik terpisah. Namun, jangan menjadikan replikasi sebagai pengganti indeks dan desain query yang baik.
3. Ada pekerjaan batch atau pekerjaan berdurasi panjang
Edge Function ideal untuk pekerjaan singkat yang selesai dalam siklus request normal. Ia bukan tempat yang tepat untuk tugas yang dapat berjalan lama, membutuhkan retry kompleks, memproses ribuan item, atau bergantung pada layanan pihak ketiga yang tidak stabil.
Contoh yang sebaiknya masuk queue:
- Mengirim notifikasi ke ribuan penerima.
- Membuat PDF, CSV, thumbnail, atau transkode media.
- Mengimpor katalog atau menyinkronkan data dari CRM.
- Memproses ulang seluruh data setelah perubahan aturan bisnis.
- Memanggil API pihak ketiga dengan rate limit dan retry.
4. Kegagalan satu proses tidak boleh menular
Jika kegagalan penyedia email, layanan OCR, atau API pembayaran non-kritis membuat endpoint aplikasi ikut lambat atau gagal, Anda memerlukan isolasi kegagalan. Queue memberi buffer antara request pengguna dan pekerjaan eksternal. Worker dapat menerapkan retry dengan backoff, batas percobaan, dead-letter queue, serta concurrency yang dibatasi.
Matriks keputusan: Realtime, Edge Functions, atau worker?
| Aspek | Realtime | Edge Functions | Queue + worker eksternal |
|---|---|---|---|
| Tujuan utama | Mendorong event ke klien yang perlu pembaruan cepat | Endpoint, webhook, validasi, dan orkestrasi singkat | Eksekusi asinkron, berat, atau tidak stabil |
| Latensi | Rendah untuk penyampaian event, tetapi bukan pengganti query data awal | Rendah hingga sedang untuk request singkat | Biasanya lebih tinggi karena antrean, tetapi terkontrol |
| Biaya | Meningkat dengan koneksi dan volume event | Meningkat dengan invokasi dan waktu eksekusi | Ada biaya broker, compute worker, penyimpanan retry, dan observabilitas |
| Kompleksitas operasional | Rendah jika channel dan filter sempit | Rendah hingga sedang | Sedang hingga tinggi: retry, idempotensi, deployment, dan monitoring |
| Keamanan | Harus membatasi akses channel dan data berdasarkan identitas pengguna | Validasi JWT dan otorisasi wajib; rahasia disimpan di environment | Service role hanya di lingkungan tepercaya; kredensial worker harus dibatasi |
| Observabilitas | Lacak koneksi, channel, event error, dan reconnect | Lacak request ID, durasi, status, dan error | Lacak kedalaman antrean, umur job, retry, job gagal, dan throughput |
| Maintainability | Baik untuk event yang sedikit dan jelas | Baik untuk batas API yang ringkas | Baik untuk domain job yang eksplisit, tetapi perlu kontrak job yang disiplin |
Aturan pilihan cepat: pilih Realtime bila pengguna yang tepat perlu tahu perubahan sekarang; pilih Edge Function bila pekerjaan selesai cepat dan hasilnya diperlukan oleh request; pilih queue dan worker bila pekerjaan dapat ditunda, gagal ulang, berjalan lama, atau perlu dibatasi concurrency-nya.
Contoh alur: upload file tanpa membebani request pengguna
Misalkan pengguna mengunggah dokumen yang harus dipindai, diekstrak metadatanya, lalu dibuat thumbnail. Jangan membuat browser menunggu semua pemrosesan selesai.
- Klien mengunggah file ke Supabase Storage dengan kontrol akses yang sesuai.
- Klien memanggil Edge Function atau endpoint aplikasi untuk membuat record
uploadsberstatusqueued. - Endpoint menyimpan job ke queue, atau mencatat outbox di Postgres yang kemudian dipompa ke queue.
- Worker mengambil job, membaca file dengan kredensial server, memprosesnya, lalu memperbarui status di Postgres menjadi
completedataufailed. - Klien yang sedang membuka halaman terkait menerima perubahan status melalui Realtime, atau melakukan refetch berkala sebagai fallback.
Perubahan status dan pembuatan outbox sebaiknya berada dalam transaksi database yang sama. Pola ini mencegah kondisi ketika record upload sudah tersimpan tetapi pesan queue gagal dibuat tanpa jejak.
-- Contoh tabel outbox yang menjadi jembatan transaksi ke queue
create table job_outbox (
id uuid primary key,
type text not null,
payload jsonb not null,
status text not null default 'pending',
created_at timestamptz not null default now(),
published_at timestamptz
);
-- Dalam transaksi yang sama dengan perubahan status upload:
-- 1. insert/update uploads
-- 2. insert job_outbox(type, payload)Sebuah publisher ringan dapat membaca item pending, mengirimnya ke broker queue, lalu menandainya published. Publisher dan worker harus idempoten: pesan bisa terkirim lebih dari sekali akibat kegagalan jaringan atau retry.
// Logika worker, disederhanakan
async function processUpload(job) {
const upload = await getUpload(job.uploadId);
if (upload.status === 'completed') return; // idempoten
await markUpload(upload.id, 'processing');
try {
const result = await scanAndCreateThumbnail(upload.storage_path);
await completeUpload(upload.id, result);
} catch (error) {
await failOrRetry(job, error);
throw error;
}
}Jangan menaruh service role key pada aplikasi browser. Worker dan Edge Function dapat menggunakannya hanya bila benar-benar perlu, dari environment server yang tepercaya. Tetap lakukan validasi tenant dan resource pada payload job; service role dapat melewati kebijakan RLS sehingga kesalahan filter tenant berisiko menjadi kebocoran lintas pelanggan.
Contoh alur: notifikasi yang cepat tetapi tetap tahan gagal
Untuk notifikasi dalam aplikasi, tulis record notifikasi ke Postgres sebagai sumber kebenaran. Realtime dapat memberi tahu pengguna yang sedang online bahwa ada notifikasi baru. Pengiriman email, push notification, atau webhook pelanggan berjalan melalui queue.
Dengan desain ini, kegagalan provider email tidak menghilangkan notifikasi dari aplikasi. Worker dapat mencoba ulang secara terkontrol, sedangkan UI tetap dapat membaca status notifikasi dari database. Hindari mengirim email langsung dalam transaksi pembuatan pesanan atau request yang juga mengubah data bisnis inti.
Strategi migrasi bertahap tanpa memecah sistem terlalu dini
Memisahkan beban kerja bukan berarti langsung membangun banyak layanan. Mulailah dengan batas kode dan kontrak data yang jelas, lalu ekstrak komponen hanya ketika ada bukti kebutuhan.
- Petakan jalur kritis. Identifikasi endpoint dan transaksi yang memengaruhi pengguna atau pendapatan. Tentukan target waktu respons dan mode kegagalannya.
- Klasifikasikan pekerjaan. Tandai setiap proses sebagai sinkron, event UI, atau asinkron. Proses yang tidak dibutuhkan untuk membentuk respons awal adalah kandidat queue.
- Tambahkan observabilitas sebelum memindahkan beban. Catat durasi request, error, query lambat, koneksi Realtime, jumlah event, serta kegagalan integrasi eksternal.
- Ekstrak satu domain job yang paling jelas. Mulai dari ekspor laporan, email, atau pemrosesan upload. Hindari memindahkan semua endpoint ke worker sekaligus.
- Gunakan kontrak job ber-versi. Sertakan tipe job, ID resource, tenant atau workspace ID, dan versi payload. Jangan mengirim objek besar atau data sensitif yang tidak perlu.
- Implementasikan idempotensi dan retry. Worker harus aman ketika job diproses dua kali. Simpan status, kunci deduplikasi, dan informasi error yang cukup untuk investigasi.
- Uji kegagalan. Simulasikan timeout provider, pesan duplikat, worker mati di tengah proses, dan job lama yang tertahan. Pastikan ada prosedur replay atau penanganan dead-letter queue.
Kesalahan umum dan cara debugging
Menggunakan Realtime untuk semua perubahan tabel
Ini sering menghasilkan event berisik dan UI yang sulit diprediksi. Debug dengan mencatat channel yang aktif, event per channel, serta halaman mana yang membuat subscription. Pastikan subscription dibersihkan ketika komponen UI tidak lagi digunakan.
Menjalankan batch dalam Edge Function
Jika request mulai timeout, sulit di-retry, atau durasinya sangat bervariasi, pindahkan batch ke worker. Edge Function dapat tetap berperan sebagai penerima request yang membuat job dan segera mengembalikan respons.
Worker langsung mengubah data tanpa batas tenant
Service role memang memudahkan akses server-ke-server, tetapi juga mengabaikan RLS. Selalu simpan dan verifikasi konteks tenant pada job, gunakan query yang spesifik terhadap resource yang dimiliki tenant tersebut, dan pisahkan kredensial berdasarkan lingkungan.
Retry tanpa idempotensi
Retry dapat mengirim email dua kali, membuat tagihan ganda, atau memperbarui status mundur. Gunakan idempotency key pada integrasi eksternal bila tersedia, dan simpan status pemrosesan secara atomik di database.
Checklist keputusan sebelum menambah worker
- Apakah pengguna benar-benar harus menunggu pekerjaan ini selesai sebelum menerima respons?
- Apakah pekerjaan dapat berjalan lama, bergantung pada API eksternal, atau membutuhkan retry?
- Apakah query atau batch ini mengganggu transaksi inti di Postgres?
- Apakah event Realtime dibutuhkan oleh pengguna aktif, atau cukup polling/refetch?
- Apakah payload job membawa tenant ID, resource ID, versi, dan kunci idempotensi?
- Apakah service role hanya berada di Edge Function atau worker tepercaya, bukan di klien?
- Apakah tersedia metrik untuk kedalaman queue, umur job, error, retry, dan dead-letter queue?
- Apakah tim memiliki runbook untuk replay job, menangani provider eksternal gagal, dan melacak request hingga job?
Arsitektur yang baik tidak memindahkan semua beban keluar dari Supabase. Ia menjaga Postgres sebagai pusat data yang konsisten, membatasi Realtime pada kebutuhan interaktif, menggunakan Edge Functions untuk batas request yang singkat, dan memberi worker ruang khusus untuk pekerjaan asinkron. Dengan pemisahan bertahap tersebut, SaaS dapat tumbuh tanpa membuat transaksi inti, keamanan, dan operasi harian menjadi rapuh.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!