DEBI PRAHARADIKA
← Back to Blog Index
System Design2026-05-3010 min read

Membangun Dashboard Real-Time Skala Enterprise dengan Server-Sent Events (SSE)

Panduan lengkap arsitektur, UI/UX esensial, dan implementasi end-to-end dashboard real-time menggunakan Server-Sent Events (SSE), TypeScript, PostgreSQL, dan TailwindCSS.

Dalam era aplikasi web modern, kebutuhan akan data instan yang mengalir langsung ke hadapan pengguna (real-time data delivery) sudah menjadi standar baku. Dashboard bisnis, pemantau infrastruktur (CPU/Memory monitor), hingga papan skor analitik kini dituntut untuk memperbarui tampilannya tanpa harus menunggu pengguna menekan tombol refresh.

Ketika dihadapkan pada skenario pembaruan data yang bersifat satu arah dari server ke klien (server-to-client unidirectional streaming), banyak pengembang secara refleks langsung memilih WebSockets. Padahal, untuk kasus penggunaan dashboard analitik, WebSockets sering kali merupakan over-engineering yang membawa beban kompleksitas operasional berlebih.

Di sinilah Server-Sent Events (SSE) menjadi solusi yang sangat elegan, efisien, dan tangguh. Artikel ini akan mengupas taksonomi arsitektur, konsep komunikasi, UI/UX, serta contoh kode implementasi praktis membangun dashboard real-time menggunakan SSE, TypeScript, PostgreSQL (LISTEN/NOTIFY), dan TailwindCSS.


1. Memahami Konsep Komunikasi SSE (Server-Sent Events)

Server-Sent Events (SSE) adalah standar teknologi web berbasis protokol HTTP yang memungkinkan server untuk mengirimkan aliran data asinkron (asynchronous event stream) secara terus-menerus (persistent) ke browser melalui satu koneksi HTTP yang bertahan lama.

Berbeda dengan WebSockets yang bersifat dua arah (bidirectional), SSE bekerja secara unidirectional (satu arah: server mengirim data, klien hanya menerima).

Cetak Biru Arsitektur SSE dengan PostgreSQL dan React

Mengapa Memilih SSE daripada WebSockets untuk Dashboard?

Untuk dashboard analitik, SSE jauh lebih unggul karena:

  • Protokol HTTP Standar: SSE berjalan di atas HTTP/1.1 atau HTTP/2 standar. Anda tidak memerlukan server khusus atau konfigurasi proxy/firewall rumit seperti yang sering terjadi pada WebSockets (ws:// atau wss://).
  • Auto-Reconnection Bawaan: Protokol SSE secara otomatis menangani pemutusan koneksi. Browser klien akan mencoba menyambung kembali (reconnect) secara berkala tanpa perlu satu baris kode JavaScript tambahan pun dari sisi Anda.
  • Efisiensi Sumber Daya Server: Sangat kompatibel dengan mekanisme kompresi HTTP standar dan mudah diintegrasikan dengan fitur connection pooling database.
  • Dukungan Last-Event-ID: Jika koneksi terputus di tengah jalan, browser akan mengirimkan header Last-Event-ID saat melakukan rekoneksi. Server dapat membaca ID tersebut dan langsung mengirimkan ulang data (replay) yang sempat terlewatkan selama masa terputusnya koneksi.

2. Desain Aliran Sistem (System Flow) & Aliran Data

Sebelum menulis kode, sangat penting untuk merancang mekanisme manajemen siklus hidup koneksi (connection lifecycle management). Tanpa arsitektur yang matang, server Anda dapat mengalami kebocoran memori (memory leak), atau browser klien mengalami hang karena akumulasi data yang tidak terkontrol (backpressure).

Diagram alir sistem (system flow) di bawah ini menggambarkan lima kondisi (state) esensial dalam menjaga stabilitas aliran data real-time:

Mekanisme Aliran Data Server-Sent Events

  • State 1: Connection Initialized (Headers Set): Server menetapkan header HTTP khusus (Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive) untuk memberi tahu server web/proxy agar tidak melakukan buffering atau caching terhadap data yang sedang dikirim.
  • State 2: Idle State & Heartbeat Triggered (Keep-Alive): Untuk mencegah penutupan koneksi secara sepihak oleh server proxy, load balancer, atau Cloudflare akibat deteksi idle connection timeout, server harus mengirimkan event kosong (heartbeat atau ping comment :\n\n) secara berkala (biasanya setiap 15 detik).
  • State 3: Connection Drop Detected & Backoff Loop: Ketika klien mendeteksi pemutusan jaringan, algoritma reconnection backoff internal browser akan mencoba menghubungkan kembali dengan jeda waktu yang terus meningkat (exponential backoff) demi melindungi server dari serangan thundering herd problem (serbuan koneksi serentak dari ribuan klien).
  • State 4: Reconnection with Last-Event-ID: Browser mengirimkan kembali ID data terakhir yang berhasil diterimanya. Backend membaca log perubahan database untuk mengirimkan kembali data yang hilang selama masa terputusnya koneksi.
  • State 5: Client Unmounts (Garbage Collection): Ketika pengguna berpindah halaman atau menutup tab dashboard, koneksi wajib dihancurkan (destroyed) baik di sisi klien maupun server untuk menghentikan pemrosesan data dan membebaskan memori server (garbage collection).

3. UI/UX Dashboard Real-Time

Membangun dashboard real-time bukan sekadar tentang seberapa cepat data terkirim, melainkan seberapa baik pengguna memahami bahwa data mereka sedang mengalir secara langsung (streaming) tanpa merusak kenyamanan visual (visual comfort).

Desain antarmuka Nexus Analytics di bawah ini mencontohkan struktur dasbor analitik real-time modern yang bersih, intuitif, dan bebas dari gangguan visual (visual noise):

Sistem Desain Antarmuka Dashboard Real-Time SSE

  1. Indikator Status Koneksi (Live Connection Status Dot):
    • Pengguna wajib mengetahui status koneksi aliran data secara instan. Pada desain di atas, status ini disajikan melalui indikator melingkar ● Live (Hijau) dengan animasi denyut halus (pulsing effect) di pojok kanan atas dasbor.
    • Dalam kondisi nyata, status ini harus berubah menjadi Reconnecting (Kuning) saat koneksi terganggu, dan Offline (Merah) jika terputus total (dilengkapi tombol "Resume Stream" untuk memicu rekoneksi tanpa reload halaman).
  2. Transisi Pembaruan Data yang Mulus (Fluid Data Transition):
    • Jangan biarkan baris data baru melompat secara kasar pada tabel transaksi. Gunakan animasi transisi masuk yang halus (fade-in animation) menggunakan Tailwind CSS untuk melunakkan pergeseran baris data baru saat menyisip ke dalam tabel.
  3. Loading Skeletons pada Metrik Utama:
    • Ketika dasbor pertama kali diakses dan koneksi SSE sedang diinisiasi, hindari menampilkan angka nol (0) yang membingungkan. Tampilkan animasi blok abu-abu kosong (loading skeletons) pada area kartu metrik (Transactions Count, Speed in TPS, dan Average Amount) untuk menjaga ekspektasi pengguna.
  4. Debouncing & Throttling UI:
    • Jika database mengalami lonjakan transaksi masif (misal: ribuan insert per detik), memperbarui antarmuka grafik dan tabel secara real-time di setiap mili-detik akan menyebabkan beban berat bagi browser (browser lag). Lakukan pembatasan (throttle) update visual maksimal 1 hingga 2 kali per detik untuk kenyamanan mata pembaca.

4. Implementasi Teknis End-to-End

Mari kita buat implementasinya. Sistem ini akan mencatat log transaksi keuangan dalam database PostgreSQL, mengirimkannya secara instan ke server TypeScript, lalu menyajikannya ke dasbor React/TailwindCSS secara real-time.

Langkah Awal: Skema Database & Mekanisme Pub/Sub PostgreSQL

Mekanisme paling efisien agar PostgreSQL dapat memberi tahu backend secara instan tanpa melakukan teknik database polling yang boros daya adalah menggunakan fitur LISTEN/NOTIFY dipadukan dengan Database Trigger.

Tulis SQL berikut pada database PostgreSQL Anda:

-- 1. Buat tabel transaksi keuangan
CREATE TABLE IF NOT EXISTS transactions (
    id SERIAL PRIMARY KEY,
    amount DECIMAL(12, 2) NOT NULL,
    category VARCHAR(50) NOT NULL,
    status VARCHAR(20) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 2. Buat fungsi trigger yang mengirimkan NOTIFY setiap ada baris baru dimasukkan
CREATE OR REPLACE FUNCTION notify_new_transaction()
RETURNS TRIGGER AS $$
BEGIN
    -- Mengirimkan payload berformat JSON ke channel 'new_transaction_event'
    PERFORM pg_notify(
        'new_transaction_event',
        json_build_object(
            'id', NEW.id,
            'amount', NEW.amount,
            'category', NEW.category,
            'status', NEW.status,
            'created_at', NEW.created_at
        )::text
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 3. Tempelkan trigger ke tabel transactions
CREATE TRIGGER after_transaction_insert
AFTER INSERT ON transactions
FOR EACH ROW
EXECUTE FUNCTION notify_new_transaction();

Sisi Backend: Server-Sent Events Route Handler (TypeScript)

Berikut adalah contoh pembuatan route handler di sisi server (Node.js/Express atau Route Handler Next.js App Router) menggunakan bahasa pemrograman TypeScript:

import { NextRequest } from 'next/server';
import pg from 'pg';

// Inisialisasi pool koneksi database PostgreSQL
const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
});

export async function GET(request: NextRequest) {
  const clientDb = await pool.connect();

  // 1. Buat response stream dengan header khusus SSE
  const responseStream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();

      // Utilitas untuk mengirim event berformat SSE standar
      const sendSSE = (id: string, event: string, data: any) => {
        const payload = `id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
        controller.enqueue(encoder.encode(payload));
      };

      // 2. Kirim sinyal inisialisasi sukses ke klien
      sendSSE('init', 'status', { message: 'Koneksi real-time berhasil terjalin.' });

      // 3. Dengarkan event 'new_transaction_event' dari PostgreSQL LISTEN/NOTIFY
      await clientDb.query('LISTEN new_transaction_event');

      const handleNotification = (msg: pg.Notification) => {
        if (msg.channel === 'new_transaction_event' && msg.payload) {
          const transactionData = JSON.parse(msg.payload);
          // Kirimkan data transaksi ke stream HTTP klien
          sendSSE(transactionData.id.toString(), 'transaction_created', transactionData);
        }
      };

      // Daftarkan listener notifikasi database
      clientDb.on('notification', handleNotification);

      // 4. Kirim data PING / Heartbeat secara berkala setiap 15 detik agar koneksi tetap hidup
      const heartbeatInterval = setInterval(() => {
        controller.enqueue(encoder.encode(':\n\n'));
      }, 15000);

      // 5. Bersihkan listener dan matikan interval ketika klien memutus koneksi
      request.signal.addEventListener('abort', () => {
        clearInterval(heartbeatInterval);
        clientDb.off('notification', handleNotification);
        clientDb.query('UNLISTEN new_transaction_event');
        clientDb.release();
        controller.close();
      });
    }
  });

  return new Response(responseStream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

Sisi Frontend: Dashboard Klien (React, TypeScript & TailwindCSS)

Berikut adalah komponen React dengan TailwindCSS yang menangani data real-time, mengelola status koneksi secara interaktif, dan merender tampilan dashboard:

import React, { useEffect, useState } from 'react';

interface Transaction {
  id: number;
  amount: number;
  category: string;
  status: string;
  created_at: string;
}

type ConnectionStatus = 'connecting' | 'connected' | 'offline';

export default function RealTimeDashboard() {
  const [transactions, setTransactions] = useState<Transaction[]>([]);
  const [status, setStatus] = useState<ConnectionStatus>('connecting');
  const [tps, setTps] = useState<number>(0); // Transactions per second

  // Inisialisasi koneksi EventSource ke SSE endpoint
  const connectSSE = () => {
    setStatus('connecting');
    const eventSource = new EventSource('/api/sse/transactions');

    // 1. Tangani ketika koneksi sukses terhubung
    eventSource.addEventListener('status', () => {
      setStatus('connected');
    });

    // 2. Tangani ketika ada data transaksi baru
    eventSource.addEventListener('transaction_created', (event) => {
      const newTx: Transaction = JSON.parse(event.data);
      
      // Update state dengan menyisipkan baris baru di paling atas
      setTransactions((prev) => [newTx, ...prev.slice(0, 19)]);
      
      // Update kalkulasi TPS secara dinamis
      setTps((prev) => parseFloat((prev + 0.15).toFixed(2)));
    });

    // 3. Tangani jika terjadi error koneksi terputus
    eventSource.onerror = () => {
      setStatus('offline');
      eventSource.close();
    };

    return eventSource;
  };

  useEffect(() => {
    const connection = connectSSE();

    // Pastikan koneksi dibersihkan saat komponen di-unmount
    return () => {
      connection.close();
    };
  }, []);

  return (
    <div className="min-h-screen bg-slate-950 text-slate-100 p-8 font-sans">
      <div className="max-w-6xl mx-auto space-y-8">
        
        {/* Header Section */}
        <div className="flex flex-col md:flex-row justify-between items-start md:items-center border-b border-slate-800 pb-6 gap-4">
          <div>
            <h1 className="text-3xl font-extrabold tracking-tight">REAL-TIME FINANCIAL STREAM</h1>
            <p className="text-slate-400 mt-1">Pemantauan transaksi keuangan instan terhubung langsung ke PostgreSQL Database.</p>
          </div>
          
          {/* Connection Status Badge */}
          <div className="flex items-center gap-3 bg-slate-900 border border-slate-800 px-4 py-2 rounded-xl">
            {status === 'connected' && (
              <>
                <span className="relative flex h-3.5 w-3.5">
                  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
                  <span className="relative inline-flex rounded-full h-3.5 w-3.5 bg-emerald-500"></span>
                </span>
                <span className="text-sm font-semibold text-emerald-400 uppercase tracking-wider">LIVE STREAMING</span>
              </>
            )}
            
            {status === 'connecting' && (
              <>
                <span className="relative flex h-3.5 w-3.5">
                  <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
                  <span className="relative inline-flex rounded-full h-3.5 w-3.5 bg-amber-500"></span>
                </span>
                <span className="text-sm font-semibold text-amber-400 uppercase tracking-wider">CONNECTING TO DATABASE</span>
              </>
            )}
            
            {status === 'offline' && (
              <>
                <span className="h-3.5 w-3.5 rounded-full bg-rose-500"></span>
                <span className="text-sm font-semibold text-rose-400 uppercase tracking-wider">OFFLINE</span>
                <button 
                  onClick={connectSSE}
                  className="ml-2 bg-rose-600 hover:bg-rose-500 text-white text-xs font-bold px-2.5 py-1 rounded transition duration-200"
                >
                  RESUME STREAM
                </button>
              </>
            )}
          </div>
        </div>

        {/* Streaming Metrics Cards */}
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
          <div className="bg-slate-900/50 border border-slate-800 p-6 rounded-2xl backdrop-blur-md hover:border-slate-700 transition duration-300">
            <h3 className="text-slate-400 text-xs font-bold tracking-widest uppercase">Volume Transaksi Aktif</h3>
            <p className="text-3xl font-extrabold mt-2 text-sky-400">{transactions.length}+ <span className="text-sm font-normal text-slate-500">records</span></p>
          </div>
          <div className="bg-slate-900/50 border border-slate-800 p-6 rounded-2xl backdrop-blur-md hover:border-slate-700 transition duration-300">
            <h3 className="text-slate-400 text-xs font-bold tracking-widest uppercase">Kecepatan Aliran (TPS)</h3>
            <p className="text-3xl font-extrabold mt-2 text-violet-400">{status === 'connected' ? tps : 0} <span className="text-sm font-normal text-slate-500">tps</span></p>
          </div>
          <div className="bg-slate-900/50 border border-slate-800 p-6 rounded-2xl backdrop-blur-md hover:border-slate-700 transition duration-300">
            <h3 className="text-slate-400 text-xs font-bold tracking-widest uppercase">Rata-Rata Nominal</h3>
            <p className="text-3xl font-extrabold mt-2 text-emerald-400">
              ${transactions.length > 0 ? (transactions.reduce((acc, curr) => acc + Number(curr.amount), 0) / transactions.length).toFixed(2) : "0.00"}
            </p>
          </div>
        </div>

        {/* Activity Feed Section */}
        <div className="bg-slate-900/40 border border-slate-800/80 rounded-2xl p-6">
          <h2 className="text-lg font-bold mb-4 tracking-tight">ALIRAN AKTIVITAS TRANSAKSI TERAKHIR</h2>
          
          <div className="space-y-3 max-h-[400px] overflow-y-auto pr-2">
            {status === 'connecting' && transactions.length === 0 ? (
              // Loading skeletons
              Array.from({ length: 4 }).map((_, idx) => (
                <div key={idx} className="animate-pulse bg-slate-900 h-16 rounded-xl border border-slate-800/50" />
              ))
            ) : transactions.length === 0 ? (
              <div className="text-center py-12 text-slate-500">Belum ada transaksi mengalir dari database.</div>
            ) : (
              transactions.map((tx) => (
                <div 
                  key={tx.id} 
                  className="flex justify-between items-center bg-slate-900/80 hover:bg-slate-900 border border-slate-800/60 p-4 rounded-xl transition duration-300 animate-fadeIn"
                >
                  <div className="flex items-center gap-3">
                    <span className={`w-2.5 h-2.5 rounded-full ${tx.status === 'success' ? 'bg-emerald-500' : 'bg-amber-500'}`} />
                    <div>
                      <p className="font-semibold text-sm">ID Transaksi #{tx.id}</p>
                      <p className="text-xs text-slate-500">{new Date(tx.created_at).toLocaleTimeString()}</p>
                    </div>
                  </div>
                  <div className="text-right">
                    <p className="font-extrabold text-sm text-sky-400">${tx.amount}</p>
                    <p className="text-xs text-slate-400 uppercase tracking-widest">{tx.category}</p>
                  </div>
                </div>
              ))
            )}
          </div>
        </div>

      </div>
    </div>
  );
}

5. Ringkasan Praktik Optimasi SSE Skala Besar

Untuk menghasilkan dashboard berbasis SSE ke tahap produksi (production scale), perhatikan aturan konfigurasi infrastruktur berikut:

  1. Gunakan HTTP/2: Pada protokol HTTP/1.1 lama, browser membatasi maksimal hanya ada 6 koneksi aktif per domain. Ini berarti jika pengguna membuka 6 tab dashboard sekaligus, browser tidak akan bisa memuat aset lain. Dengan beralih ke HTTP/2, batas ini dilewati karena seluruh data dialirkan dalam satu koneksi tunggal (multiplexing).
  2. Konfigurasi Nginx Proxy: Jika Anda menggunakan proxy server seperti Nginx di depan aplikasi Node.js, Anda wajib mematikan fitur buffering pada Nginx agar data SSE terkirim secara instan tanpa ditahan sementara. Tambahkan konfigurasi berikut pada blok lokasi Nginx Anda:
    proxy_set_header Connection '';
    proxy_http_version 1.1;
    chunked_transfer_encoding off;
    proxy_buffering off;
    proxy_cache off;
    
  3. Connection Pooling Database: Selalu gunakan connection pooling seperti PgBouncer pada PostgreSQL untuk menangani ribuan koneksi konkuren, mencegah habisnya batas slot koneksi database saat jumlah klien dashboard meledak secara eksponensial.

Dengan arsitektur yang andal, pengelolaan siklus hidup koneksi yang bersih, serta implementasi UI/UX yang matang, Server-Sent Events (SSE) terbukti menjadi solusi ideal dalam membangun sistem dashboard real-time yang cepat, stabil, dan hemat biaya infrastruktur.