Cara Menggunakan Push Notification dengan Firebase Cloud Messaging di Next.js 15

Bikin aplikasi web yang interaktif itu belum afdol kalau belum ada fitur pemberitahuan langsung ke layar user. Masalahnya, banyak developer yang baru terjun ke framework modern sering pusing tujuh keliling saat mencoba pasang fitur ini. Kasus yang paling sering terjadi adalah pemberitahuannya lancar jaya waktu aplikasi dibuka, tapi langsung mlempem dan hilang arah begitu tab browser atau aplikasinya ditutup sama user.

Saya ingat banget, waktu pertama kali ngulik Firebase Cloud Messaging (FCM) di Next.js beberapa waktu lalu, saya sempat begadang sampai jam 3 pagi cuma gara-gara error pelik yang bunyinya messaging/noble-arguments. Eh, ternyata masalahnya sepele banget: saya salah menaruh file Service Worker dan salah memahami cara kerja siklus hidup komponen React di sisi client. Nah, supaya kamu nggak perlu mengalami drama kurang tidur yang sama, kita bakal bedah habis-habisan cara pasang FCM di Next.js 15—saya pakai Node.js 20 dan Firebase versi terbaru saat menulis panduan ini—dari nol sampai bisa kirim pemberitahuan berdasarkan kelompok tertentu, bahkan saat aplikasi sedang ditutup rapat oleh user.

1. Persiapan Awal dan Setup Console Firebase

Sebelum masuk ke baris kode, langkah pertama yang wajib kamu selesaikan adalah menyiapkan rumahnya terlebih dahulu di platform cloud. Buka konsol utama Firebase menggunakan akun Google kamu, lalu buat sebuah proyek baru. Beri nama proyek tersebut sesuai dengan aplikasi yang sedang kamu kembangkan. Proses penyiapan di dasbor ini biasanya memakan waktu sekitar satu sampai dua menit.

Setelah masuk ke dasbor utama proyek, daftarkan aplikasi web baru dengan mengklik ikon tanda kurung siku (fitur web). Di sini, Firebase akan memberikan beberapa baris kode berisi barisan kunci rahasia. Jangan khawatir, kita tidak akan menyalin mentah-mentah kode tersebut ke dalam komponen utama kita karena hal itu bisa membuat data aplikasi kita rentan dieksploitasi jika tidak sengaja terunggah ke repositori publik.

Tampilan menu Web Push certificates di dalam Console Firebase untuk membuat VAPID key

Gambar 1: Lokasi pembuatan pasang kunci VAPID di dalam dasbor setelan Firebase.

Sekarang, masuk ke bagian setelan proyek (ikon gerigi di kiri atas) lalu pilih tab bernama Cloud Messaging. Gulir halaman sedikit ke bawah sampai kamu menemukan bagian Web Configuration. Di sana, kamu akan melihat menu bertuliskan Web Push certificates. Klik tombol bertuliskan "Generate key pair" untuk memunculkan string acak yang sangat panjang. String inilah yang disebut sebagai VAPID Key. Kunci ini bertindak sebagai jabat tangan digital agar browser bawaan komputer user tahu bahwa server kamulah yang mengirimkan pesan tersebut, bukan pihak asing yang berniat mengirimkan spam.

Langkah penutup di bagian persiapan ini adalah membuat file konfigurasi lokal di komputer kamu. Buka proyek Next.js 15 yang sudah kamu siapkan, lalu buat file bernama .env.local tepat di bagian paling luar (root folder). Masukkan semua kunci rahasia dari dasbor tadi dengan format seperti ini:

NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyA1X-ContohKeyYangMunculDiConsole
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=nama-proyek-kamu.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=nama-proyek-kamu
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=nama-proyek-kamu.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789012
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789012:web:abcdef123456
NEXT_PUBLIC_FIREBASE_VAPID_KEY=BOb3R-StringVapidKeyYangSangatPanjangDanAcakDariTabCloudMessaging

Penggunaan awalan NEXT_PUBLIC_ di sini wajib dilakukan karena kode FCM kita nantinya berjalan sepenuhnya di sisi browser milik user, sehingga framework Next.js perlu membagikan nilai variabel lingkungan ini ke sisi client secara aman.

2. Install dan Konfigurasi Firebase di Next.js 15

Mari buka terminal bawaan editor kodemu, lalu pasang paket resmi bawaan Firebase. Kita cukup memasang satu paket utama ini saja karena semua kebutuhan autentikasi, database, hingga pesan instan sudah dibundel di dalamnya secara modular.

npm install firebase

Setelah proses unduhan selesai, saatnya membuat jembatan penghubung antara proyek Next.js dengan server Google. Buat sebuah folder baru di dalam direktori utama kamu, misalnya dengan jalur src/utils/firebase.js. Di sinilah letak trik mendasar agar aplikasi kamu tidak mengalami error re-initialization yang sering membuat aplikasi macet saat fitur hot-reload berjalan di server lokal.

Saya pribadi lebih suka menggunakan pendekatan kondisional yang bersih untuk memisahkan siklus server dan client. Mari kita susun kodenya seperti ini:

// src/utils/firebase.js
import { initializeApp, getApps, getApp } from "firebase/app";
import { getMessaging, getToken, onMessage } from "firebase/messaging";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

// Cek apakah aplikasi Firebase sudah pernah diinisialisasi sebelumnya
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();

// FCM murni membutuhkan objek window, jadi pastikan ini hanya aktif di browser
const messaging = typeof window !== "undefined" ? getMessaging(app) : null;

export { messaging, getToken, onMessage };

Perhatikan baris kode typeof window !== "undefined" di atas. Ini berdasarkan pengalaman praktis saya, bukan data resmi, tapi melewatkan baris pengecekan ini di Next.js versi App Router dijamin akan memicu error fatal saat server mencoba merender halaman. Pasalnya, server Next.js tidak mengenal apa itu objek pesan bawaan browser, sehingga proses build aplikasi dipastikan akan gagal total jika bagian ini tidak diamankan dengan benar.

3. Membuat Service Worker untuk Background Handling

Sekarang kita masuk ke bagian inti yang paling krusial untuk menjawab kebutuhan agar aplikasi tetap bisa menerima kiriman pesan meski tab browser sudah ditutup rapat oleh user. Rahasianya terletak pada komponen bernama Service Worker. Ini adalah skrip latar belakang yang berjalan terpisah dari halaman web utama kamu.

Sebelum menulis kodenya, kamu harus paham aturan main struktur folder di Next.js 15. Skrip Service Worker untuk urusan pesan ini wajib ditaruh di dalam folder public/ dengan nama file yang kaku, tidak boleh diubah satu huruf pun, yaitu firebase-messaging-sw.js. Mengapa harus di folder public? Karena browser perlu mengunduh file ini dari jalur akar aplikasi (root URL) agar ia memiliki hak akses penuh dalam mengontrol notifikasi sistem.

Berikut adalah bagan alur bagaimana pesan dari server bisa mendarat di perangkat user secara senyap:

[ Server Firebase Cloud Messaging ]
               │
               ▼ (Pesan dikirim lewat jalur internet)
┌────────────────────────────────────────────────────────┐
│              Sistem Operasi / Browser User             │
│                                                        │
│  ┌──────────────────────────────────────────────────┐  │
│  │ public/firebase-messaging-sw.js                  │  │
│  │ (Berjalan otomatis di balik layar sistem)        │  │
│  └──────────────────────────────────────────────────┘  │
│               │                                        │
│               ▼                                        │
│     [ Muncul Tampilan Pop-up Notifikasi Layar ]        │
└────────────────────────────────────────────────────────┘

Buka folder public/ kamu, buat file bernama firebase-messaging-sw.js, lalu masukkan kode berikut. Di sini kita menggunakan library dengan format lama (compat) milik Firebase karena lingkungan Service Worker bawaan browser belum mendukung skrip modul modern secara langsung tanpa alat bantu kompilasi tambahan.

// public/firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js');
importScripts('https://www.gstatic.com/firebasejs/10.8.0/firebase-messaging-compat.js');

// Inisialisasi ulang di dalam Service Worker menggunakan data mentah
firebase.initializeApp({
  apiKey: "AIzaSyA1X-ContohKeyYangMunculDiConsole",
  authDomain: "nama-proyek-kamu.firebaseapp.com",
  projectId: "nama-proyek-kamu",
  storageBucket: "nama-proyek-kamu.appspot.com",
  messagingSenderId: "123456789012",
  appId: "1:123456789012:web:abcdef123456"
});

const messaging = firebase.messaging();

// Fungsi penangkap pesan ketika aplikasi mati atau berada di latar belakang
messaging.onBackgroundMessage((payload) => {
  console.log('[SW] Ada pesan masuk dari latar belakang: ', payload);

  const judulNotifikasi = payload.notification.title;
  const opsiNotifikasi = {
    body: payload.notification.body,
    icon: '/ikon-notif.png', // letakkan file gambar ikon-notif.png di folder public kamu
  };

  self.registration.showNotification(judulNotifikasi, opsiNotifikasi);
});

Ya, kamu tidak salah lihat. Di dalam file Service Worker ini, kita terpaksa menuliskan kembali barisan konfigurasi Firebase secara langsung (hardcode) alih-alih mengambilnya dari file .env.local. Ya sudah, ikuti saja cara ini karena skrip latar belakang browser ini memang terisolasi total dan tidak bisa membaca variabel lingkungan milik Next.js. Pastikan nilai-nilainya sama persis dengan yang kamu tulis di langkah pertama tadi agar komunikasinya tidak salah alamat.

4. Meminta Izin Notifikasi dan Mengambil Token

Setelah urusan Service Worker di folder publik beres, sekarang kita kembali ke komponen React untuk membuat alur interaksi dengan user. Aturan dasar dari sistem browser modern adalah kita tidak boleh menembak token tanpa izin eksplisit dari user. Jika kamu langsung memunculkan pop-up izin begitu halaman pertama kali dimuat, sebagian besar user akan merasa terganggu dan langsung memilih tombol "Blokir". Oleh karena itu, pendekatan terbaik adalah menggunakan tombol pemicu tepercaya atau komponen khusus yang rapi.

Mari kita buat sebuah komponen client baru bernama NotificationRegister.jsx di dalam direktori komponen kamu. Komponen ini akan mendeteksi apakah browser mendukung fitur pesan bawaan, meminta akses secara resmi, lalu menampilkan token unik perangkat ke layar agar bisa kita pakai untuk uji coba.

// src/components/NotificationRegister.jsx
"use client";

import { useEffect, useState } from "react";
import { messaging, getToken, onMessage } from "@/utils/firebase";

export default function NotificationRegister() {
  const [tokenFCM, setTokenFCM] = useState("");
  const [statusIzin, setStatusIzin] = useState("");

  useEffect(() => {
    // Pastikan skrip hanya jalan di client-side dan messaging aktif
    if (!messaging) return;

    setStatusIzin(Notification.permission);

    // Menangani pesan yang masuk saat aplikasi sedang dibuka (Foreground)
    const unsubscribe = onMessage(messaging, (payload) => {
      console.log("Ada pesan masuk (Aplikasi Aktif):", payload);
      alert(`[Notif Baru]: ${payload.notification.title}\n${payload.notification.body}`);
    });

    return () => unsubscribe();
  }, []);

  const aktifkanNotifikasi = async () => {
    try {
      const izin = await Notification.requestPermission();
      setStatusIzin(izin);

      if (izin === "granted") {
        // Ambil token registrasi perangkat dari server Firebase
        const tokenTerbaru = await getToken(messaging, {
          vapidKey: process.env.NEXT_PUBLIC_FIREBASE_VAPID_KEY,
        });

        if (tokenTerbaru) {
          setTokenFCM(tokenTerbaru);
          console.log("Token Perangkat Kamu (Simpan ini):", tokenTerbaru);
          
          // SINKRONISASI: Di sini kamu bisa kirim token ke database backend kamu
          // contoh: await kirimTokenKeDatabase(tokenTerbaru);
        } else {
          alert("Gagal dapet token. Cek apakah VAPID Key kamu sudah benar.");
        }
      } else {
        alert("Yah, kamu menolak izin notifikasi. Fitur ini nggak bakal jalan.");
      }
    } catch (error) {
      console.error("Terjadi masalah saat mendaftarkan perangkat:", error);
    }
  };

  return (
    <div style={{ padding: '25px', border: '2px dashed #0070f3', borderRadius: '8px', margin: '20px 0' }}>
      <h3>Konfigurasi Push Notification</h3>
      <p>Status Izin Browser Saat Ini: <strong>{statusIzin}</strong></p>
      
      {statusIzin !== "granted" ? (
        <button 
          onClick={aktifkanNotifikasi}
          style={{ background: '#0070f3', color: '#fff', padding: '10px 20px', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
        >
          Aktifkan Notifikasi Sekarang
        </button>
      ) : (
        <p style={{ color: 'green' }}>✓ Perangkat kamu sudah terdaftar dan siap menerima pesan.</p>
      )}

      {tokenFCM && (
        <div style={{ marginTop: '15px' }}>
          <p style={{ fontSize: '12px', margin: '5px 0 0 0' }}><strong>Token FCM Kamu:</strong></p>
          <code style={{ display: 'block', background: '#eee', padding: '10px', wordBreak: 'break-all', fontSize: '11px', borderRadius: '4px' }}>
            {tokenFCM}
          </code>
        </div>
      )}
    </div>
  );
}

Eh ternyata, ada satu kejadian lucu waktu saya memasang kode ini di Next.js 15. Saya lupa kalau Next.js menerapkan sistem pra-render secara ketat. Waktu pertama kali tombolnya diklik, konsol browser saya langsung merah semua menampilkan error Notification is not defined. Nah, solusinya ya pembungkusan di dalam kait useEffect seperti kode di atas, atau kamu bisa memuat komponen ini secara dinamis dengan opsi ssr: false milik Next.js agar server tidak rewel.

5. Mengirim Notifikasi Berdasarkan Segmen User

Mengirimkan satu pesan ke seluruh pengguna aplikasi secara acak adalah cara tercepat untuk membuat rating aplikasi kamu anjlok karena dianggap mengganggu. Di sinilah pentingnya fitur segmentasi atau pengelompokan berdasarkan ketertarikan (Topik). Sayangnya, karena alasan keamanan privasi data, Firebase Web SDK versi terbaru sudah tidak mengizinkan perangkat client untuk langsung mendaftarkan dirinya ke suatu topik tertentu (fungsi subscribeToTopic di sisi frontend sudah dihapus).

Solusi terbaiknya adalah dengan membuat satu gerbang API mini (Route Handler) di dalam proyek Next.js 15 kita. Jadi, frontend akan mengirimkan token perangkat ke API lokal kita, lalu sisi server kita yang akan menembak REST API milik Google menggunakan kunci server utama yang aman.

Mari buat file handler baru di jalur src/app/api/subscribe/route.js:

// src/app/api/subscribe/route.js
import { NextResponse } from 'next/server';

export async function POST(request) {
  try {
    const { token, topik } = await request.json();

    if (!token || !topik) {
      return NextResponse.json({ error: 'Token dan nama topik wajib diisi' }, { status: 400 });
    }

    // Panggil REST API Google Instance ID untuk mendaftarkan token ke topik tertentu
    const response = await fetch(`https://iid.googleapis.com/v1/web/iid/${token}/rel/topics/${topik}`, {
      method: 'POST',
      headers: {
        'Authorization': `key=KUNCI_SERVER_LAMA_DARI_CONSOLE_FIREBASE`, 
        'Content-Type': 'application/json'
      }
    });

    if (response.ok) {
      return NextResponse.json({ 
        success: true, 
        message: `Hore! Perangkat kamu berhasil dimasukkan ke segmen kelompok: ${topik}` 
      });
    }

    return NextResponse.json({ error: 'Gagal mendaftarkan segmen ke server Firebase' }, { status: 400 });
  } catch (error) {
    return NextResponse.json({ error: error.message }, { status: 500 });
  }
}

Saya lebih suka menggunakan cara menjembatani lewat API internal seperti ini daripada memasang seluruh berkas kredensial admin sekelas firebase-admin ke dalam project jika kebutuhannya cuma sebatas segmentasi sederhana. Struktur ini jauh lebih ringan dan mempercepat waktu tunggu prapemrosesan server.

Setelah sistem API lokal ini berjalan, kamu tinggal menguji pengiriman lewat dasbor utama Google. Masuk ke Firebase Console, pilih menu Cloud Messaging, buat kampanye pemberitahuan baru, tulis isi pesannya, lalu pada bagian Target ganti opsi menjadi "Topic" dan ketik nama kelompok yang sesuai dengan parameter yang kamu kirimkan lewat API tadi. Klik tombol kirim, dan lihat hasilnya dalam hitungan detik!


6. Selanjutnya Bisa Dikembangkan

Sistem dasar penanganan notifikasi kita, baik saat web terbuka maupun saat tertutup rapat, kini sudah menyala dengan sempurna. Agar aplikasi kamu semakin mirip dengan platform skala besar milik perusahaan teknologi papan atas, berikut beberapa ide pengembangan menarik yang bisa kamu coba selanjutnya:

  • Penyimpanan Token Terpusat: Hubungkan token unik perangkat yang didapat dari sisi client ke dalam tabel database user (seperti Supabase atau Prisma) agar kamu bisa menembak notifikasi personal berdasarkan aktivitas spesifik user tersebut.
  • Aksi Klik Kustom (Click Action): Modifikasi parameter di dalam berkas Service Worker agar ketika pop-up notifikasi diklik di layar komputer, browser otomatis membuka tab baru dan mengarahkan user ke rute halaman ulasan produk atau artikel yang relevan.
  • Otomatisasi Cron-Job: Integrasikan pengiriman pesan via API internal ini dengan sistem penjadwalan otomatis (seperti Vercel Cron atau GitHub Actions) untuk mengirimkan rangkuman berita mingguan ke user secara berkala tanpa perlu kamu klik manual setiap hari.

Kalau ada baris kode yang bikin kamu bingung atau tiba-tiba muncul error misterius di konsol browser waktu kamu mencoba praktik, langsung tanyakan saja di kolom komentar di bawah halaman ini. Saya bakal bantu balas secepatnya, biasanya dalam kurun waktu kurang dari 24 jam kalau saya kebetulan lagi nggak tidur atau nggak sedang pusing dikejar target rilis aplikasi klien! 😄


Daftar Referensi

  1. Dokumentasi Resmi Firebase Cloud Messaging
  2. MDN Web Docs: Service Worker API untuk Pengembang Web
  3. Dokumentasi Resmi Next.js Framework Versi Terbaru
Ruang Debug
Bingung pasang push notification di Next.js 15? Yuk pelajari cara konfigurasi Firebase Cloud Messaging, handle notifikasi saat app ditutup, & kirim ..

Komentar