Next.js
Add relaybell to a Next.js App Router site: load the SDK with next/script, call init() once, serve sw.js from public/ and link signed-in users.
The browser SDK is a script tag, not an npm package. In the App Router you load it with next/script from a small client component in your root layout, start it once in onLoad, and serve relaybell's service worker from public/. Your Project ID is on the dashboard's Install page.
1. Add the service worker
Download sw.js and save it as public/sw.js. Next.js serves everything in public/ from the root of your site, so it opens at https://yoursite.com/sw.js — the default swPath. Browsers only run a service worker from your own domain, so push can't work without this file.
Note
Already have a service worker at /sw.js (from a PWA plugin, for example)? Keep it. Save relaybell's as public/relaybell/sw.js instead and pass swPath: "/relaybell/sw.js" and swScope: "/relaybell/" to init() — see Installing the SDK.
2. Load the SDK once
Create a client component that loads the script after the page is interactive and calls RelayBell.init() when it has loaded:
// app/relaybell.tsx
"use client";
import Script from "next/script";
let started = false; // init() once per page load
export function RelayBell() {
return (
<Script
src="https://relaybell.com/relaybell-sdk.js"
strategy="afterInteractive"
onLoad={() => {
if (started) return;
started = true;
window.RelayBell?.init({ projectId: "YOUR_PROJECT_ID", apiBase: "https://relaybell.com" });
// Your own signal for code that waits for the SDK (see "Link signed-in users").
window.dispatchEvent(new Event("relaybell:loaded"));
}}
/>
);
}Render it once, in the root layout, so it stays loaded while visitors move between pages:
// app/layout.tsx
import { RelayBell } from "./relaybell";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<RelayBell />
</body>
</html>
);
}Call init() once per page. A second call starts again from its own options, so don't put it in a useEffect of a page component. onLoad runs once, when the script has loaded; client-side navigation doesn't load it again.
The Project ID isn't a secret (it's in your page's HTML), so process.env.NEXT_PUBLIC_RELAYBELL_PROJECT_ID is fine for it. Your API key (rb_live_…) is: never put it in a NEXT_PUBLIC_ variable or a client component — use it only in server code, with the server SDKs.
Note
If your site sends a Content-Security-Policy header, it must at least allow the script and the API calls: https://relaybell.com in script-src and connect-src.
3. TypeScript types
The SDK puts RelayBell on window. Declare the parts you use, for example in types/relaybell.d.ts:
export {};
declare global {
interface Window {
RelayBell?: {
init(options: {
projectId: string;
apiBase?: string;
swPath?: string;
swScope?: string;
externalId?: string;
userHash?: string;
tags?: Record<string, string | number | boolean>;
requireConsent?: boolean;
policyVersion?: string;
autoCapture?: boolean | { pageviews?: boolean; clicks?: boolean; scroll?: boolean };
}): Promise<unknown>;
subscribeNow(): Promise<{ supported: boolean; subscribed: boolean; permission?: NotificationPermission; reason?: string }>;
isSubscribed(): Promise<boolean>;
identify(externalId: string | null, userHash?: string): Promise<void>;
unsubscribe(): Promise<{ unsubscribed: boolean }>;
track(event: string, properties?: Record<string, unknown>): Promise<unknown>;
};
}
}Every option and method is described in the Browser SDK reference.
4. Link signed-in users
To send to one of your users by your own user id (Send to one user), tell the SDK who is signed in. Make the signature (userHash) on the server — it's the HMAC-SHA256 of the id, keyed with your website's secret key from Settings → Developers → User ID verification (Identity verification):
// lib/relaybell-hash.ts: server code only (the secret key must never reach the browser)
import { createHmac } from "node:crypto";
export function relaybellUserHash(userId: string) {
return createHmac("sha256", process.env.RELAYBELL_IDENTITY_SECRET!).update(String(userId)).digest("hex");
}A small client component hands the id and its signature to the SDK, waiting for the script if it hasn't loaded yet (on the first page it loads after the page becomes interactive):
// app/relaybell-identify.tsx
"use client";
import { useEffect } from "react";
export function RelayBellIdentify({ userId, userHash }: { userId: string; userHash: string }) {
useEffect(() => {
const run = () => window.RelayBell?.identify(userId, userHash);
if (window.RelayBell) run();
else window.addEventListener("relaybell:loaded", run, { once: true });
return () => window.removeEventListener("relaybell:loaded", run);
}, [userId, userHash]);
return null;
}Render it from a Server Component that knows who is signed in, for example the layout of your signed-in area:
// app/(account)/layout.tsx
import { RelayBellIdentify } from "../relaybell-identify";
import { relaybellUserHash } from "@/lib/relaybell-hash";
import { getCurrentUser } from "@/lib/auth"; // your own sign-in code
export default async function AccountLayout({ children }: { children: React.ReactNode }) {
const user = await getCurrentUser();
return (
<>
{user ? <RelayBellIdentify userId={String(user.id)} userHash={relaybellUserHash(String(user.id))} /> : null}
{children}
</>
);
}Only ever sign the id of the user who is signed in on that request: an endpoint that signs any id it is given would let anyone claim any user. identify() can run before init() has finished, and calling it again with the same values does nothing.
On a shared browser, call window.RelayBell?.unsubscribe() when someone signs out of a site without identity verification — see identify() under Methods.
5. A subscribe button (optional)
relaybell shows your opt-in prompt by itself (set it up under Opt-in prompts). For your own button, call subscribeNow() from the click:
"use client";
import { useEffect, useState } from "react";
export function NotifyButton() {
const [on, setOn] = useState(false);
useEffect(() => {
const check = () => window.RelayBell?.isSubscribed().then(setOn);
if (window.RelayBell) check();
else window.addEventListener("relaybell:loaded", check, { once: true });
return () => window.removeEventListener("relaybell:loaded", check);
}, []);
if (on) return <p>Notifications are on.</p>;
return (
<button
onClick={async () => {
const result = await window.RelayBell?.subscribeNow();
if (result?.subscribed) setOn(true);
}}
>
Turn on notifications
</button>
);
}Check it works
Open your site over HTTPS (or http://localhost), accept the prompt, then send a test from the dashboard. Things to look for:
https://yoursite.com/sw.jsopens and shows relaybell's service worker.- The browser console has no
relaybell:warnings. - With
autoCapture: true, route changes in your app are recorded aspage_viewevents too: the SDK notices client-side navigation by itself.