@clickstream/next
Official Next.js adapter. Bridges the gap between the browser-side @clickstream/signals library and Next.js's server surface — Server Components, Route Handlers, and edge middleware.
Peer dep: next ^13 || ^14 || ^15.
Runtime dep: @clickstream/signals.
Why you want this
@clickstream/signals runs in the browser, which means the visitor snapshot isn't available until after the page hydrates. If you want to gate SSR output on a score — show <HighIntentOffer /> server-side when intent >= 70, render the default pricing otherwise — you need the snapshot at render time.
This adapter reads the first-party _cs_uid cookie on the incoming request, fetches the VisitorContext from your first-party collector, and returns it synchronously (well, async-ly) inside your Server Component.
Install
pnpm add @clickstream/next @clickstream/react @clickstream/signals
Edge middleware (optional but recommended)
Install at middleware.ts in your app root:
// middleware.ts
import { clickStreamMiddleware } from '@clickstream/next/middleware';
export default clickStreamMiddleware({
apiKey: process.env.CLICKSTREAM_API_KEY!,
endpoint: 'https://t.example.com',
prefetch: true,
});
export const config = {
matcher: ['/pricing', '/cart', '/checkout/:path*'],
};
What it does on every matched request:
- Reads the
_cs_uidcookie fromrequest.cookies. - Validates the visitor id (regex + length cap against header-injection shenanigans).
- Stashes the id on
x-clickstream-visitor-idso Server Components + Route Handlers can read it without re-parsing cookies. - When
prefetch: true, round-trips to/v1/signals/:visitorIdand stashes the fullVisitorContextonx-clickstream-context(base64-encoded JSON).getServerVisitor()then returns the cached context with zero network round-trips.
Only enable prefetch: true on routes where every SSR render actually gates on server-side scores — the prefetch adds ~50–150 ms per request. On the rest of your site, omit prefetch and let getServerVisitor() fetch on demand.
Middleware options
| Option | Type | Default | Notes |
|---|---|---|---|
apiKey | string | required | ClickStream API key. |
endpoint | string | required | Your first-party tracking domain. |
prefetch | boolean | false | Round-trip to the signals endpoint on every matched request. |
prefetchTimeoutMs | number | 500 | Abort the prefetch after N ms so it doesn't stall SSR. |
getServerVisitor()
Read the visitor context in any async Server Component:
// app/pricing/page.tsx
import { getServerVisitor } from '@clickstream/next/server';
export default async function PricingPage() {
const { visitor, source } = await getServerVisitor({
apiKey: process.env.CLICKSTREAM_API_KEY!,
endpoint: 'https://t.example.com',
});
// Fallback to default UI when no cookie is set, the visitor's DO instance
// aged out, or the endpoint couldn't be reached.
if (!visitor) return <DefaultPricing />;
if (visitor.scores.intent >= 70) return <HighIntentOffer />;
if (visitor.scores.frustration >= 60) return <SupportPromo />;
return <DefaultPricing />;
}
Or in a Route Handler:
// app/api/offer/route.ts
import { getServerVisitor } from '@clickstream/next/server';
export async function GET() {
const { visitor } = await getServerVisitor({
apiKey: process.env.CLICKSTREAM_API_KEY!,
endpoint: 'https://t.example.com',
});
return Response.json({
plan: (visitor?.scores.intent ?? 0) >= 70 ? 'premium' : 'basic',
});
}
Resolution order
getServerVisitor() reads from (in order of preference):
x-clickstream-contextheader — set by the middleware whenprefetch: true. Zero network round-trip.x-clickstream-visitor-idheader — set by the middleware on every matched request. Adapter fetches/v1/signals/:visitorIdon your first-party domain.- Raw
_cs_uidcookie — fallback when middleware isn't installed. Adapter reads the cookie vianext/headers#cookies()and fetches. - Nothing — returns
{ visitor: null, source: 'no_cookie' }.
Return value
interface ServerVisitorResult {
visitor: VisitorContext | null;
source: 'header' | 'fetch' | 'no_cookie' | 'not_active' | 'error' | 'stub';
}
Never throws. Unauthorized / plan-gated / network errors all land on { visitor: null, source: 'error' } so page code can fall back to defaults without a try/catch.
Client-side hydration
The server adapter and the React adapter compose cleanly. Wrap your root in <ClickStreamProvider> from @clickstream/react so client components can keep the snapshot warm after hydration:
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
// app/providers.tsx
'use client';
import { ClickStreamProvider } from '@clickstream/react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ClickStreamProvider
apiKey={process.env.NEXT_PUBLIC_CLICKSTREAM_KEY!}
endpoint="https://t.example.com"
>
{children}
</ClickStreamProvider>
);
}
Subpath imports
import { clickStreamMiddleware } from '@clickstream/next/middleware';
import { getServerVisitor } from '@clickstream/next/server';
Two separate entries because middleware runs on the edge runtime (no next/headers) while the server helper runs on the node runtime (uses next/headers via dynamic import()). Keeping them in separate entry files prevents edge bundlers from pulling in node-only modules.
Pages Router
getServerVisitor() works in getServerSideProps via the resolvers argument:
import { getServerVisitor } from '@clickstream/next/server';
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const { visitor } = await getServerVisitor(
{
apiKey: process.env.CLICKSTREAM_API_KEY!,
endpoint: 'https://t.example.com',
},
{
headers: () => ({ get: (name) => ctx.req.headers[name.toLowerCase()] as string | null }),
cookies: () => ({
get: (name: string) => (ctx.req.cookies[name] ? { value: ctx.req.cookies[name] as string } : undefined),
}),
},
);
return { props: { visitor } };
}
The middleware factory is App Router-shaped — Pages Router doesn't have an equivalent edge middleware surface.
See also
- React adapter — client-side hooks + provider
- Signals API —
VisitorContextshape - Install — full install matrix