Database

Supabase Realtime Features

Tutorial komprehensif Supabase Realtime — channel broadcast untuk komunikasi client-to-client, presence tracking, database changes streaming, Row Level Security (RLS), dan Edge Functions untuk backend serverless

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.

Arsitektur Supabase Realtime
📱
Client Apps
Browser, mobile,
server
→ WebSocket →
Realtime Server
Broadcast, Presence,
DB Changes
🐘
PostgreSQL
Logical replication
+ RLS
🔒
RLS Policies
Authorization per-row
sebelum data dikirim

1.1 Setup

JavaScript — Setup Supabase Client
// 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.

JavaScript — Broadcast untuk Real-time Communication
// 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.

JavaScript — Presence untuk Online Status
// 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 Adalah Ephemeral

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.

JavaScript — Listening Database Changes
// 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 Requirements

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.

SQL — Row Level Security Policies
-- 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.

TypeScript — Edge Function
// 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' }
    });
});
Terminal — Deploy Edge Function
# 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

JavaScript — Complete Chat Implementation
// 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

AspekRekomendasi
Broadcast vs DB ChangesGunakan broadcast untuk data ephemeral, DB changes untuk data persisten
Channel CleanupSelalu unsubscribe saat komponen unmount
RLS PoliciesSelalu aktifkan RLS untuk tabel yang diakses client langsung
Filter Specific EventsFilter event (INSERT/UPDATE/DELETE) dan filter column untuk mengurangi noise
Connection LimitsBatasi jumlah concurrent channels (free: 200, pro: 500)
Rate LimitingAtur eventsPerSecond untuk mencegah flooding
Error HandlingImplementasi reconnection logic untuk channel yang disconnect

9. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut:

Pertanyaan 1: Apa perbedaan antara Broadcast dan Database Changes di Supabase Realtime?

a) Tidak ada perbedaan
b) Broadcast untuk komunikasi client-to-client (ephemeral), Database Changes untuk streaming perubahan dari tabel PostgreSQL
c) Broadcast lebih lambat
d) Database Changes tidak mendukung filter

Pertanyaan 2: Apa fungsi dari Row Level Security (RLS)?

a) Mengenkripsi data di database
b) Menentukan baris mana yang bisa diakses oleh setiap user
c) Mengelola backup database
d) Mengoptimalkan query performance

Pertanyaan 3: Apa yang terjadi jika user disconnect saat menggunakan Presence?

a) Data presence tetap di database selamanya
b) Presence data hilang setelah timeout (ephemeral)
c) User harus login ulang
d) Semua user lain ter-disconnect

Pertanyaan 4: Apa runtime yang digunakan oleh Supabase Edge Functions?

a) Node.js
b) Python
c) Deno
d) Go

Pertanyaan 5: Mengapa RLS sangat penting di Supabase?

a) Karena mempercepat query
b) Karena client berinteraksi langsung dengan database — tanpa RLS semua data terbuka
c) Karena wajib oleh PostgreSQL
d) Karena mengurangi biaya hosting
← Sebelumnya Redis Streams untuk Event Sourcing Selanjutnya → CockroachDB: Distributed SQL
🔍 Zoom
100%
🎨 Tema