Hero
Back to all guides

Consent management with JKT48Connect in Next.js

Learn how to queue JKT48Connect API calls and analytics events until the user gives consent, then flush everything at once with a single call in your Next.js app.

JKT48Connect Team

3/2/2026

Updated on 3/2/2026

Beginner
15 min

Consent management with JKT48Connect in Next.js

Privacy regulations like GDPR and CCPA require that you obtain explicit user consent before tracking behaviour or collecting user data. This guide shows how to gate your JKT48Connect analytics integration behind a consent check in Next.js — holding all tracking until the user makes a choice, then enabling everything at once — or discarding it silently on decline.

Prerequisites

  • A Next.js project (App Router or Pages Router)
  • Your JKT48Connect API key from the JKT48Connect portal

Initialise with consent check

Create a client-side module that reads the stored consent choice before initialising the JKT48Connect client. If consent hasn't been granted yet, set a flag to disable tracking so no data leaves the browser prematurely.

lib/jkt48.ts
'use client';

const BASE_URL = 'https://v2.jkt48connect.com';
const API_KEY = process.env.NEXT_PUBLIC_JKT48_API_KEY!;

let trackingEnabled = false;
const eventQueue: Array<{ endpoint: string; payload?: object }> = [];

export function isTrackingEnabled() {
  return trackingEnabled;
}

export async function jkt48Track(endpoint: string, payload?: object) {
  if (!trackingEnabled) {
    eventQueue.push({ endpoint, payload });
    return;
  }
  await sendEvent(endpoint, payload);
}

async function sendEvent(endpoint: string, payload?: object) {
  await fetch(`${BASE_URL}${endpoint}?apikey=${API_KEY}`, {
    method: 'GET',
    ...payload,
  });
}

export function enableTracking() {
  trackingEnabled = true;
  // Flush all queued events
  while (eventQueue.length > 0) {
    const item = eventQueue.shift()!;
    sendEvent(item.endpoint, item.payload);
  }
}

From this point on, any jkt48Track(...) calls elsewhere in your app are safely queued and not transmitted until consent is given.

Create a Client Component for the consent UI. The key is to call enableTracking() when the user accepts, and do nothing when they decline.

components/ConsentBanner.tsx
'use client';

import { useEffect, useState } from 'react';
import { enableTracking } from '@/lib/jkt48';

export function ConsentBanner() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const stored = localStorage.getItem('jkt48_consent');
    if (!stored) setVisible(true);
    if (stored === 'granted') enableTracking();
  }, []);

  function handleAccept() {
    localStorage.setItem('jkt48_consent', 'granted');
    enableTracking(); // flushes the queue and enables all future tracking
    setVisible(false);
  }

  function handleDecline() {
    localStorage.setItem('jkt48_consent', 'denied');
    setVisible(false); // queue is discarded, nothing is sent
  }

  if (!visible) return null;

  return (
    <div role="dialog" aria-label="Cookie consent">
      <p>
        We use JKT48Connect to display JKT48 data in this app. Do you consent
        to anonymous usage tracking?
      </p>
      <button type="button" onClick={handleAccept}>Accept</button>
      <button type="button" onClick={handleDecline}>Decline</button>
    </div>
  );
}

Call enableTracking() on consent

enableTracking() does two things:

  1. Sets the trackingEnabled flag so all future fetch calls are sent immediately
  2. Flushes the entire queue — every event buffered since the module was loaded is sent at once

This means you don't lose any events that happened before the user made their choice. Any data requests made while the banner was visible are captured and sent the moment they consent.

Mount the banner in your layout

Add the ConsentBanner to your root layout so it appears on every page. Because it's a Client Component, it won't affect server rendering.

App Router

app/layout.tsx
import { ConsentBanner } from '@/components/ConsentBanner';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <ConsentBanner />
      </body>
    </html>
  );
}

Pages Router

pages/_app.tsx
import type { AppProps } from 'next/app';
import { ConsentBanner } from '@/components/ConsentBanner';

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <ConsentBanner />
    </>
  );
}

Handle decline

If the user declines, don't call enableTracking(). The queue lives only in memory and is automatically discarded when the tab closes or the page navigates away. No data is ever sent to the JKT48Connect API.

function handleDecline() {
  localStorage.setItem('jkt48_consent', 'denied');
  // trackingEnabled stays false — nothing will be sent
  // The in-memory queue will be garbage collected
}

Persist consent across page loads

The trackingEnabled flag resets on every page load because Next.js re-imports modules fresh on navigation. You need to read the stored consent choice inside a useEffect on initialisation and call enableTracking() immediately if consent was already granted.

The ConsentBanner component already handles this in its useEffect:

useEffect(() => {
  const stored = localStorage.getItem('jkt48_consent');
  if (!stored) setVisible(true);          // no choice yet — show banner
  if (stored === 'granted') enableTracking(); // already accepted — enable immediately
}, []);

This ensures returning visitors never see the banner again and tracking resumes instantly without re-prompting.

Full example

lib/jkt48.ts
'use client';

const BASE_URL = 'https://v2.jkt48connect.com';
const API_KEY = process.env.NEXT_PUBLIC_JKT48_API_KEY!;

let trackingEnabled = false;
const eventQueue: Array<{ endpoint: string; payload?: object }> = [];

export function enableTracking() {
  trackingEnabled = true;
  while (eventQueue.length > 0) {
    const item = eventQueue.shift()!;
    fetch(`${BASE_URL}${item.endpoint}?apikey=${API_KEY}`);
  }
}

export async function jkt48Fetch(endpoint: string) {
  if (!trackingEnabled) {
    eventQueue.push({ endpoint });
    return null;
  }
  const res = await fetch(`${BASE_URL}${endpoint}?apikey=${API_KEY}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}
components/ConsentBanner.tsx
'use client';

import { useEffect, useState } from 'react';
import { enableTracking } from '@/lib/jkt48';

export function ConsentBanner() {
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const stored = localStorage.getItem('jkt48_consent');
    if (!stored) setVisible(true);
    if (stored === 'granted') enableTracking();
  }, []);

  if (!visible) return null;

  return (
    <div role="dialog" aria-label="Cookie consent">
      <p>We use JKT48Connect to power this app. Do you consent?</p>
      <button
        type="button"
        onClick={() => {
          localStorage.setItem('jkt48_consent', 'granted');
          enableTracking();
          setVisible(false);
        }}
      >
        Accept
      </button>
      <button
        type="button"
        onClick={() => {
          localStorage.setItem('jkt48_consent', 'denied');
          setVisible(false);
        }}
      >
        Decline
      </button>
    </div>
  );
}
app/layout.tsx
import { ConsentBanner } from '@/components/ConsentBanner';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <ConsentBanner />
      </body>
    </html>
  );
}

Loved by JKT48 developers everywhere

From personal fansites to large fan communities, JKT48Connect helps developers build better JKT48 apps — without the scraping headache.

  • Rizky Pratama
    Rizky Pratama
    @rizkypratama_dev

    Baru integrasi @JKT48Connect ke fansite aku dan literally took less than 10 menit buat dapetin data member + jadwal teater sekaligus.

    No more scraping yang sering break. Ini yang selama ini dicari! 🔥

  • Fadhil Hakim
    Fadhil Hakim
    @fadhildev48

    JKT48Connect is a beast. Data member lengkap, live stream real-time, theater schedule — all in one API.

    Dulu habis waktu berhari-hari bikin scraper. Sekarang cukup satu npm install.

  • Alicia Ramadhani
    Alicia Ramadhani
    @alicia_wota

    Sebagai fans yang juga developer, JKT48Connect benar-benar game changer.

    Birthday API-nya keren banget — bisa langsung bikin reminder ulang tahun oshi tanpa hitung manual. Dokumentasinya juga sangat jelas ✨

  • Bintang Nugroho
    Bintang Nugroho
    @bintang_codes

    Discord bot JKT48 aku sekarang pakai JKT48Connect dan hasilnya jauh lebih stable.

    Webhook-nya works perfectly, langsung notif ke server kalau member lagi live di IDN.

  • Kavya Indraswari
    Kavya Indraswari
    @kavya_jkt48fan

    🤯 Baru deploy fansite pakai @JKT48Connect dan dalam 1 jam udah ada data member dari semua generasi, jadwal show minggu ini, dan live stream tracking.

    TypeScript SDK-nya autocomplete semua endpoint. Ini yang namanya developer experience! #jkt48 #webdev

  • Dimas Aryanto
    Dimas Aryanto
    @dimas_wr

    Udah 3 bulan pakai JKT48Connect buat app tracker oshi aku.

    Belum pernah sekalipun data-nya salah atau API-nya down pas lagi dibutuhkan.

    Worth every rupiah. 🙌

  • Naufal Keanu
    Naufal Keanu
    @naufalkeanu

    Fansite JKT48 tim kami sekarang fully powered by @JKT48Connect.

    Dari yang taunya scraping manual, sekarang data theater, live stream, dan news semua real-time.

    Kalau kamu mau bikin sesuatu tentang JKT48, ini wajib dipakai 🚀

Ready to build something for JKT48?
Start integrating in minutes

Join developers and fanbases using JKT48Connect. Get your API key for free and start accessing real-time JKT48 data today.

Read the docs