---
title: "Add Togo to a Next.js app"
description: "Install the browser SDK in an App Router app, record page views, and send server events from route handlers."
canonical: "https://www.togohq.ai/docs/nextjs"
applies_to: "@togohq/sdk-browser 1.x, @togohq/sdk-node 1.x"
updated: "2026-11-03T00:00:00Z"
---

# Add Togo to a Next.js app

Install the browser SDK in an App Router app, record page views, and send server events from route handlers.

This page installs Togo by hand. To have your coding agent do it instead, see
[Install Togo with your AI agent](/docs/install).

## Install the SDK

```bash
npm install @togohq/sdk-browser
```

Put your write keys in environment variables. Use the **test** key (`togo_pkt_…`) in
`.env.local` and preview deployments, and the **live** key (`togo_pkl_…`) in production. Both are
public, so the `NEXT_PUBLIC_` prefix is correct.

```bash
# .env.local
NEXT_PUBLIC_TOGO_WRITE_KEY=togo_pkt_your_test_key
```

## Create one client

The browser SDK remembers one person, so it belongs in the browser only. Create the client the
first time browser code asks for it:

```ts
// lib/analytics.ts
'use client';

import { init, type Client } from '@togohq/sdk-browser';

let client: Client | undefined;

export function analytics(): Client {
  client ??= init({ writeKey: process.env.NEXT_PUBLIC_TOGO_WRITE_KEY! });
  return client;
}
```

`init()` returns at once and never throws. If you call it during server rendering it returns a
client that does nothing and logs one warning, which is why the calls below sit in effects and
event handlers.

## Record page views

Start the client as early as possible — the first event carries the referrer and the campaign
parameters, which are gone once the visitor navigates — and send a page view on every route
change. Add this component to your root layout:

```tsx
// app/togo-page-views.tsx
'use client';

import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { analytics } from '@/lib/analytics';

export function TogoPageViews() {
  const pathname = usePathname();
  useEffect(() => {
    analytics().track('page_viewed');
  }, [pathname]);
  return null;
}
```

```tsx
// app/layout.tsx
import { TogoPageViews } from './togo-page-views';

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

Every event already carries the page's URL, path, referrer and campaign, so `page_viewed` needs
no properties. The query string and `#fragment` are never sent.

## Track what people do

Call `track()` where the thing happens:

```tsx
'use client';

import { analytics } from '@/lib/analytics';

export function UpgradeButton() {
  return (
    <button onClick={() => analytics().track('upgrade_clicked', { plan: 'pro' })}>Upgrade</button>
  );
}
```

Use event names you already have, or lowercase `snake_case` for new ones. Property values are
strings, numbers, booleans or `null` — never an email address or anything a person typed
([What never goes in an event](/docs/concepts/personal-data)).

## Identify people at sign-in

```ts
analytics().identify(user.id); // after sign-in
analytics().reset(); // on sign-out
```

Use your own stable user id, the same one your server uses.
[Identify people when they sign in](/docs/guides/identify) covers the details.

## Send events from route handlers

Things that happen on the server — a payment confirmed, a webhook received — are sent with
`@togohq/sdk-node`. On Vercel and other serverless platforms, **`await analytics.flush()` before
you return**, or the platform may freeze the function with the events still queued.

```ts
// app/api/checkout/route.ts
import { init } from '@togohq/sdk-node';

const analytics = init({ writeKey: process.env.TOGO_WRITE_KEY! });

export async function POST(request: Request) {
  const { userId } = await request.json();
  // ... complete the checkout ...
  analytics.track('checkout_completed', { plan: 'pro' }, { userId });
  await analytics.flush();
  return Response.json({ ok: true });
}
```

The server SDK names the person on every call. [Node.js and serverless](/docs/node) covers it.

## Check it works

Run the app with the test key, click around, and confirm the events arrived:
[Check that events arrive](/docs/verify).
