Skip to main content

Quickstart

:::caution Pre-release This is a pre-1.0 library (v0.1.1) — API may change without notice. :::

Get up and running in three steps: middleware, collect route, and client initialization.

Step 1: Middleware — Mint a Token

In your Next.js middleware (middleware.ts), use withJourneyToken to issue a secure token to the client:

import { withJourneyToken, type JourneyTokenPayload } from "@next-story/journey-recorder/server";
import { NextResponse, type NextRequest } from "next/server";

export function middleware(request: NextRequest) {
// Pass the request through to mint a journey token
const response = NextResponse.next();

return withJourneyToken({
request,
response,
tokenSecrets: {
current: process.env.NS_TOKEN_SECRET!,
previous: process.env.NS_TOKEN_SECRET_PREVIOUS,
},
siteResolver: (token) => ({
siteId: "my-site",
domain: process.env.NS_COOKIE_DOMAIN || "localhost",
}),
}).response;
}

export const config = {
matcher: ["/((?!_next|static|favicon.ico).*)"],
};

This middleware runs on every request and adds a __ns_token cookie to the response.

Step 2: Collect Route

Create a Route Handler at app/api/journey/collect/route.ts:

import { createCollectHandler } from "@next-story/journey-recorder/server";
import { NextRequest } from "next/server";

const handler = createCollectHandler({
tokenSecrets: {
current: process.env.NS_TOKEN_SECRET!,
previous: process.env.NS_TOKEN_SECRET_PREVIOUS,
},
sink: {
async storeEvents(batch) {
// TODO: persist batch.events to your database
console.log("Received events:", batch.events);
},
},
ipSalt: process.env.NS_IP_SALT!,
});

export async function POST(request: NextRequest) {
return handler(request);
}

export const runtime = "edge"; // Edge-safe, no Node.js dependencies

Step 3: Client Initialization

In a client component, initialize the journey recorder:

"use client";
import { initJourney } from "@next-story/journey-recorder/client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";

export function JourneyRecorder() {
const pathname = usePathname();

useEffect(() => {
const journey = initJourney({
siteId: "my-site",
collectUrl: "/api/journey/collect",
conversionRoutePattern: "/checkout",
});

journey.reportRoute(pathname);

return () => journey.stop();
}, [pathname]);

return null;
}

Add this component to your layout:

import { JourneyRecorder } from "@/components/journey-recorder";

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
<JourneyRecorder />
{children}
</body>
</html>
);
}

That's it

You're now recording:

  • Page views — when the user navigates to a route
  • Rage clicks — rapid repeated clicks on the same target
  • Dead clicks — clicks with no observable effect
  • Conversions — either via journey.track("conversion") or automatically on conversionRoutePattern

See Events & Data Model for details on what gets recorded.