[relaybell docs › Install › Next.js](https://relaybell.com/docs/install-nextjs) · [All docs as Markdown](https://relaybell.com/docs.md) · [Index for AI assistants](https://relaybell.com/llms.txt)

# 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 {#service-worker}

Download [sw.js](https://relaybell.com/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](https://relaybell.com/docs/install).

## 2. Load the SDK once {#load-the-sdk}

Create a client component that loads the script after the page is interactive and calls `RelayBell.init()` when it has loaded:

```tsx
// 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:

```tsx
// 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](https://relaybell.com/docs/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 {#types}

The SDK puts `RelayBell` on `window`. Declare the parts you use, for example in `types/relaybell.d.ts`:

```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](https://relaybell.com/docs/browser-sdk) reference.

## 4. Link signed-in users {#identify}

To send to one of your users by your own user id ([Send to one user](https://relaybell.com/docs/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](https://relaybell.com/docs/identity)):

```ts
// 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):

```tsx
// 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:

```tsx
// 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](https://relaybell.com/docs/browser-sdk#sdk-methods).

## 5. A subscribe button (optional) {#subscribe-button}

relaybell shows your opt-in prompt by itself (set it up under [Opt-in prompts](https://relaybell.com/docs/prompts)). For your own button, call `subscribeNow()` from the click:

```tsx
"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 {#check}

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.js` opens and shows relaybell's service worker.
- The browser console has no `relaybell:` warnings.
- With `autoCapture: true`, route changes in your app are recorded as `page_view` events too: the SDK notices client-side navigation by itself.
