> ## Documentation Index
> Fetch the complete documentation index at: https://www.glyphhq.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Wire Glyph into a Next.js App Router app — client and server.

A Next.js app can use both SDKs:

* **`@glyphhq/node` on the server** — route handlers, server actions, and webhooks. Put payments, subscription changes, and other critical events here because delivery does not depend on a browser session.
* **`@glyphhq/browser` on the client** — in-product actions and page views that only exist client-side.

If you only instrument one side, use the server. Add the browser SDK when page views or client-only actions provide useful context.

## Environment variables

```bash .env.local theme={"dark"}
# Dedicated server key: never shipped to the browser.
GLYPH_WRITE_KEY=glyph_test_...

# Dedicated browser key: NEXT_PUBLIC_ makes it available in client bundles.
NEXT_PUBLIC_GLYPH_WRITE_KEY=glyph_test_...
```

Use `glyph_test_...` keys in development and `glyph_pk_...` in production. Create separate named keys for the server and browser. A browser key is publicly visible, so separation lets you [rotate](/docs/reference/write-keys#reveal-and-rotation) it without changing server deployments.

## Server side

Create one shared client at module scope. Each server process gets one instance, allowing it to batch messages across requests:

```ts lib/glyph.ts theme={"dark"}
import { GlyphNode } from "@glyphhq/node";

const writeKey = process.env.GLYPH_WRITE_KEY;
if (!writeKey) {
  throw new Error("GLYPH_WRITE_KEY is not set");
}

export const glyph = new GlyphNode({
  writeKey,
  onError: (error, messages, { status, retryable }) => {
    console.error("Glyph delivery failed", { status, retryable });
  },
});
```

### Route handlers and server actions

Serverless runtimes can freeze the moment a response is sent, so flush before returning:

```ts app/api/projects/route.ts theme={"dark"}
import { NextResponse } from "next/server";
import { glyph } from "@/lib/glyph";

export async function POST(request: Request) {
  const project = await createProject(request);

  await glyph.track(project.ownerId, "Project created", {
    projectId: project.id,
  });
  await glyph.flush();

  return NextResponse.json(project);
}
```

The same pattern applies inside server actions: track, `await glyph.flush()`, return.

<Note>
  Use the Node.js runtime (the default) for routes that send to Glyph. If a route opts into another runtime, verify it supports the timer and `fetch` APIs the SDK relies on before shipping.
</Note>

### Webhooks

Billing webhooks are a reliable place to capture payment and subscription events:

```ts app/api/webhooks/stripe/route.ts theme={"dark"}
import { glyph } from "@/lib/glyph";

export async function POST(request: Request) {
  const event = await verifyStripeWebhook(request);

  switch (event.type) {
    case "invoice.paid":
      await glyph.track(userIdFor(event), "Invoice paid", {
        amount: event.data.object.amount_paid,
        currency: event.data.object.currency,
      });
      break;
    case "customer.subscription.deleted":
      await glyph.track(userIdFor(event), "Subscription cancelled");
      break;
    default:
      break;
  }

  await glyph.flush();
  return new Response(null, { status: 200 });
}
```

### Profile on signup

Send the profile from the server when signup completes, such as in an auth callback or server action:

```ts theme={"dark"}
await glyph.profile(user.id, {
  email: user.email,
  name: user.name,
  plan: "free",
});
await glyph.flush();
```

## Client side

Manage the singleton in a client component near the root and initialize it only for signed-in users. Glyph only tracks [known customers](/docs/guides/what-to-capture#known-customers-only). Reinitializing or resetting disposes the previous client, clears its queue, and removes its timer and browser listeners:

```tsx components/glyph-provider.tsx theme={"dark"}
"use client";

import { init, reset } from "@glyphhq/browser";
import { useEffect } from "react";

const writeKey = process.env.NEXT_PUBLIC_GLYPH_WRITE_KEY;
if (!writeKey) {
  throw new Error("NEXT_PUBLIC_GLYPH_WRITE_KEY is not set");
}

export function GlyphProvider({ userId }: { userId: string | null }) {
  useEffect(() => {
    if (!userId) {
      reset(); // drop the queue on logout — nothing sends for signed-out visitors
      return;
    }
    init({
      writeKey,
    });
  }, [userId]);

  return null;
}
```

Render it from your root layout with the session's user ID (from your auth library, server-side). Then track from any client component:

```tsx theme={"dark"}
"use client";

import { getClient } from "@glyphhq/browser";

export function ExportButton({ userId }: { userId: string }) {
  return (
    <button
      type="button"
      onClick={() => {
        getClient()
          .track(userId, "Report exported", { format: "csv" })
          .catch(() => undefined);
      }}
    >
      Export
    </button>
  );
}
```

### Page views

App Router navigation doesn't reload the page, so track route changes with `usePathname`:

```tsx components/glyph-pageview.tsx theme={"dark"}
"use client";

import { getClient } from "@glyphhq/browser";
import { usePathname } from "next/navigation";
import { useEffect } from "react";

export function GlyphPageview({ userId }: { userId: string | null }) {
  const pathname = usePathname();

  useEffect(() => {
    if (!userId) {
      return;
    }
    getClient()
      .page(userId, undefined, { path: pathname })
      .catch(() => undefined);
  }, [userId, pathname]);

  return null;
}
```

Track only product areas that add useful context; see [page views worth capturing](/docs/guides/what-to-capture#capture-page-views-where-they-happen). App Router transitions preserve the page and its in-memory queue. A hard navigation or tab close only triggers a best-effort flush, so critical events should remain server-side.

## Checklist

1. Separate server and browser keys set through `GLYPH_WRITE_KEY` and `NEXT_PUBLIC_GLYPH_WRITE_KEY` in both `.env.local` and your deployment's environment.
2. Shared `GlyphNode` in `lib/glyph.ts`, with `onError` logging.
3. `await glyph.flush()` before every response in route handlers, server actions, and webhooks.
4. `profile` sent server-side at signup, with `email`.
5. Client singleton initialized only for signed-in users; `reset()` on logout.
6. Verified with a `glyph_test_...` key in the test environment before switching production to `glyph_pk_...`.

Verify each item, then check the resulting customer timelines in the test environment.
