1. Pengenalan Supabase Realtime
Supabase adalah platform Backend-as-a-Service (BaaS) open-source yang dibangun di atas PostgreSQL. Salah satu fitur paling powerful dari Supabase adalah Realtime — kemampuan untuk mendengarkan perubahan data dan mengirimkan update ke client secara real-time melalui WebSocket.
Supabase Realtime memiliki tiga fitur utama: Broadcast (komunikasi client-to-client), Presence (tracking status user), dan Database Changes (streaming perubahan tabel PostgreSQL). Ketiganya diakses melalui channel system yang berjalan di atas WebSocket.
server
DB Changes
+ RLS
sebelum data dikirim
1.1 Setup
// Install
// npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = 'https://your-project.supabase.co';
const supabaseAnonKey = 'your-anon-key';
const supabase = createClient(supabaseUrl, supabaseAnonKey, {
realtime: {
params: {
eventsPerSecond: 10 // Rate limit events
}
}
});
// Cek koneksi realtime
const channel = supabase.channel('test-connection');
channel.subscribe((status) => {
console.log('Realtime status:', status);
// 'SUBSCRIBED' = connected
// 'TIMED_OUT' = timeout
// 'CLOSED' = disconnected
// 'CHANNEL_ERROR' = error
});
2. Broadcast Channels
Broadcast memungkinkan komunikasi client-to-client secara real-time tanpa menyimpan data di database. Cocok untuk fitur seperti live cursor, chat sementara, notifikasi instan, dan collaborative editing.
// Membuat channel broadcast
const chatChannel = supabase.channel('room-1', {
config: {
broadcast: {
self: true, // Terima pesan dari diri sendiri
ack: true // Tunggu acknowledgement dari server
}
}
});
// Subscribe ke channel
chatChannel
.on('broadcast', { event: 'message' }, (payload) => {
console.log('Pesan baru:', payload);
const { sender, text, timestamp } = payload.payload;
appendMessage(sender, text, timestamp);
})
.on('broadcast', { event: 'typing' }, (payload) => {
showTypingIndicator(payload.payload.user);
})
.subscribe((status) => {
if (status === 'SUBSCRIBED') {
console.log('Terhubung ke room chat!');
}
});
// Mengirim pesan
await chatChannel.send({
type: 'broadcast',
event: 'message',
payload: {
sender: 'Budi',
text: 'Halo semua!',
timestamp: new Date().toISOString()
}
});
// Typing indicator
await chatChannel.send({
type: 'broadcast',
event: 'typing',
payload: { user: 'Budi' }
});
// Unsubscribe saat komponen unmount
// chatChannel.unsubscribe();
// ========================================
// Collaborative cursor (live cursor)
// ========================================
const docChannel = supabase.channel('doc-collab');
docChannel
.on('broadcast', { event: 'cursor_move' }, (payload) => {
const { userId, x, y } = payload.payload;
updateCursorUI(userId, x, y);
})
.subscribe();
// Mengirim posisi cursor (throttle!)
function onCursorMove(x, y) {
docChannel.send({
type: 'broadcast',
event: 'cursor_move',
payload: { userId: myUserId, x, y }
});
}
3. Presence Tracking
Presence melacak status online/offline user dalam channel. Cocok untuk fitur "who's online", status availability, dan real-time user lists.
// Channel dengan presence
const roomChannel = supabase.channel('room-1', {
config: {
presence: {
key: user.id, // Unique key per user
}
}
});
// Listen untuk perubahan presence
roomChannel
.on('presence', { event: 'sync' }, () => {
const state = roomChannel.presenceState();
console.log('Online users:', state);
// Format: { 'user-1': [{ ... }, ...], 'user-2': [{ ... }] }
updateOnlineUsersList(state);
})
.on('presence', { event: 'join' }, ({ key, newPresences }) => {
console.log(`${key} joined:`, newPresences);
showNotification(`${newPresences[0].username} joined the room`);
})
.on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
console.log(`${key} left:`, leftPresences);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track presence setelah subscribe
await roomChannel.track({
user_id: user.id,
username: user.name,
avatar_url: user.avatar,
online_at: new Date().toISOString(),
status: 'active' // active, idle, away
});
}
});
// Update status
async function setStatus(newStatus) {
await roomChannel.track({
user_id: user.id,
username: user.name,
status: newStatus,
updated_at: new Date().toISOString()
});
}
// Mendapatkan state saat ini
const currentState = roomChannel.presenceState();
const onlineCount = Object.keys(currentState).length;
// Cleanup: untrack saat user keluar
// window.addEventListener('beforeunload', () => {
// roomChannel.untrack();
// });
Presence data tidak disimpan di database — hanya berada di memory Realtime server. Jika user disconnect, presence-nya akan hilang setelah timeout. Gunakan presence untuk status sementara (online/offline, cursor), bukan untuk data penting.
4. Database Changes
Database Changes memungkinkan Anda mendengarkan perubahan pada tabel PostgreSQL secara real-time. Setiap INSERT, UPDATE, atau DELETE pada tabel yang di-subscribe akan mengirimkan event ke client melalui WebSocket.
// Subscribe ke perubahan pada tabel 'messages'
const messagesChannel = supabase
.channel('db-messages')
.on(
'postgres_changes',
{
event: '*', // '*', 'INSERT', 'UPDATE', 'DELETE'
schema: 'public',
table: 'messages'
},
(payload) => {
console.log('Change received!', payload);
const { eventType, new: newRow, old: oldRow } = payload;
switch (eventType) {
case 'INSERT':
appendMessage(newRow);
break;
case 'UPDATE':
updateMessage(newRow);
break;
case 'DELETE':
removeMessage(oldRow.id);
break;
}
}
)
.subscribe();
// Filter hanya INSERT tertentu
supabase
.channel('new-orders')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'orders',
filter: 'status=eq.pending' // Filter dari RLS atau payload
},
(payload) => {
showNewOrderNotification(payload.new);
}
)
.subscribe();
// Filter berdasarkan user_id
supabase
.channel('my-notifications')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'notifications',
filter: `user_id=eq.${currentUser.id}`
},
(payload) => {
showNotification(payload.new);
}
)
.subscribe();
// Multiple tables
const allChangesChannel = supabase
.channel('all-db-changes')
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, handleNewPost)
.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'posts' }, handleUpdatePost)
.on('postgres_changes', { event: '*', schema: 'public', table: 'comments' }, handleCommentChange)
.subscribe();
Database Changes membutuhkan Realtime replication yang diaktifkan pada tabel. Di Supabase Dashboard → Database → Replication → aktifkan untuk tabel yang dibutuhkan. Setiap tabel yang di-subscribe menggunakan one database connection — terlalu banyak subscriptions bisa menghabiskan connection pool.
5. Row Level Security (RLS)
Row Level Security (RLS) adalah fitur PostgreSQL yang diintegrasikan erat dengan Supabase. RLS memungkinkan Anda membuat policies yang menentukan baris mana yang bisa diakses oleh setiap user — sangat penting untuk keamanan ketika client langsung berinteraksi dengan database.
-- Aktifkan RLS pada tabel
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
-- Policy: user bisa membaca pesan di channel yang mereka ikuti
CREATE POLICY "Users can read messages in their channels"
ON messages FOR SELECT
USING (
channel_id IN (
SELECT channel_id FROM channel_members
WHERE user_id = auth.uid()
)
);
-- Policy: user bisa menginsert pesan di channel yang mereka ikuti
CREATE POLICY "Users can insert messages in their channels"
ON messages FOR INSERT
WITH CHECK (
channel_id IN (
SELECT channel_id FROM channel_members
WHERE user_id = auth.uid()
)
AND sender_id = auth.uid()
);
-- Policy: user hanya bisa menghapus pesan sendiri
CREATE POLICY "Users can delete their own messages"
ON messages FOR DELETE
USING (sender_id = auth.uid());
-- Policy: user hanya bisa mengupdate pesan sendiri
CREATE POLICY "Users can update their own messages"
ON messages FOR UPDATE
USING (sender_id = auth.uid())
WITH CHECK (sender_id = auth.uid());
-- Tabel profiles: semua bisa baca, hanya sendiri yang bisa update
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT USING (true);
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE USING (auth.uid() = id);
-- Service role bypass RLS (untuk server-side operations)
-- Supabase client dengan service_role key bypass RLS
6. Edge Functions
Edge Functions adalah serverless functions yang berjalan di edge (Deno runtime) — cocok untuk API endpoints, webhooks, scheduled tasks, dan logic yang terlalu kompleks untuk client-side.
// supabase/functions/process-order/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
serve(async (req) => {
const { order_id } = await req.json();
// Buat Supabase client dengan service_role
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
);
// 1. Ambil data order
const { data: order, error } = await supabase
.from('orders')
.select('*, items(*)')
.eq('id', order_id)
.single();
if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// 2. Proses order (contoh: validasi stok)
for (const item of order.items) {
const { data: product } = await supabase
.from('products')
.select('stock')
.eq('id', item.product_id)
.single();
if (product!.stock < item.quantity) {
return new Response(JSON.stringify({
error: `Insufficient stock for product ${item.product_id}`
}), { status: 400 });
}
}
// 3. Update status order
await supabase
.from('orders')
.update({ status: 'processing', processed_at: new Date().toISOString() })
.eq('id', order_id);
// 4. Kurangi stok
for (const item of order.items) {
await supabase.rpc('decrement_stock', {
p_id: item.product_id,
p_qty: item.quantity
});
}
return new Response(JSON.stringify({
success: true,
order_id,
status: 'processing'
}), {
headers: { 'Content-Type': 'application/json' }
});
});
# Login ke Supabase
supabase login
# Inisialisasi project
supabase init
# Buat edge function
supabase functions new process-order
# Test locally
supabase functions serve process-order --env-file ./supabase/.env.local
# Deploy ke Supabase
supabase functions deploy process-order
# Memanggil edge function dari client
curl -X POST https://your-project.supabase.co/functions/v1/process-order \
-H "Authorization: Bearer your-anon-key" \
-H "Content-Type: application/json" \
-d '{"order_id": "123"}'
# Dari JavaScript:
# const { data, error } = await supabase.functions.invoke('process-order', {
# body: { order_id: '123' }
# });
7. Real-time Patterns
7.1 Chat Application
// Chat dengan kombinasi DB changes + broadcast + presence
const channel = supabase.channel(`chat:${roomId}`, {
config: { presence: { key: userId } }
});
// 1. Real-time messages dari database
channel.on('postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'messages',
filter: `room_id=eq.${roomId}` },
(payload) => {
renderMessage(payload.new);
scrollToBottom();
}
);
// 2. Typing indicator via broadcast
channel.on('broadcast', { event: 'typing' }, ({ payload }) => {
if (payload.user_id !== userId) {
showTypingIndicator(payload.username);
}
});
// 3. Online users via presence
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState();
updateOnlineCount(Object.keys(state).length);
});
// Subscribe
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({ user_id: userId, username, online_at: new Date() });
}
});
// Send message (insert ke DB, trigger realtime ke semua client)
async function sendMessage(text) {
await supabase.from('messages').insert({
room_id: roomId,
user_id: userId,
text: text
});
}
8. Best Practices & Limits
| Aspek | Rekomendasi |
|---|---|
| Broadcast vs DB Changes | Gunakan broadcast untuk data ephemeral, DB changes untuk data persisten |
| Channel Cleanup | Selalu unsubscribe saat komponen unmount |
| RLS Policies | Selalu aktifkan RLS untuk tabel yang diakses client langsung |
| Filter Specific Events | Filter event (INSERT/UPDATE/DELETE) dan filter column untuk mengurangi noise |
| Connection Limits | Batasi jumlah concurrent channels (free: 200, pro: 500) |
| Rate Limiting | Atur eventsPerSecond untuk mencegah flooding |
| Error Handling | Implementasi reconnection logic untuk channel yang disconnect |
9. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut: