On this page
Next.js
Last updated
Integrate vTilt with Next.js — App Router and Pages Router, npm package or inline script, client + server-side tracking with the Browser and Node SDKs.
vTilt works in any Next.js app (App Router or Pages Router) and across both runtimes. The Browser SDK handles client-side analytics, autocapture, and session recording; the Node SDK lets you emit events from server actions, route handlers, and middleware.
#1. Add your environment variables
Put your token and ingest origin in .env.local. Anything read in the browser must be prefixed NEXT_PUBLIC_.
NEXT_PUBLIC_VTILT_TOKEN=YOUR_PROJECT_TOKEN
NEXT_PUBLIC_VTILT_HOST=https://www.vtilt.com
VTILT_TRACKER_TOKEN=YOUR_PROJECT_TOKEN#2. Install & initialise
Two supported paths — pick one, don't combine them. The inline script stub (dashboard Integration Script) needs no dependency and is the simplest default. The npm package (@v-tilt/browser) is a clean fit for an already-bundled Next.js app that prefers typed imports and a pinned version. Both run the same vt.init() and expose the same API.
No package to install. Load the SDK stub with next/script and strategy="beforeInteractive" so vt.init() runs before the first paint:
// app/layout.tsx
import Script from 'next/script'
import { VTiltProvider } from './providers/vtilt-provider'
import { getCurrentUser } from '@/lib/auth'
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
const currentUser = await getCurrentUser()
return (
<html lang="en">
<body>
<Script id="vtilt-script" strategy="beforeInteractive">
{`
!function(t,e){var o,n,p,r;function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}if(!e.__SV&&!(window.vt&&window.vt.__loaded)){window.vt=e,e._i=[],o="init capture identify setUserProperties resetUser getUserIdentity getDeviceId getUserState alias getConfig getSessionId updateConfig setConsent on once off startAutocapture stopAutocapture startSessionRecording stopSessionRecording openChat closeChat toggleChat showChat hideChat sendChatMessage gtag setGoogleUserData".split(" ");for(n=0;n<o.length;n++)g(e,o[n]);e.init=function(i,s,a){(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.script_host?s.script_host+"/array.js":s.api_host+"/dist/array.js",p.onerror=function(){console.error("vTilt: Failed to load library script:",p.src)},(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="vt",u.toString=function(t){var e="vt";return"vt"!==a&&(e+="."+a),t||(e+=" (stub)"),e},n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1}}(document,window.vt||[]);
vt.init('${process.env.NEXT_PUBLIC_VTILT_TOKEN}', {
api_host: '${process.env.NEXT_PUBLIC_VTILT_HOST}',
autocapture: true,
capture_pageview: true,
capture_pageleave: true,
});
`}
</Script>
<VTiltProvider currentUser={currentUser}>{children}</VTiltProvider>
</body>
</html>
)
}For the Pages Router, put the same <Script> in pages/_app.tsx.
#3. Track route changes
App Router client navigation goes through the History API, which the SDK already patches (pushState / replaceState), so SPA pageviews fire automatically. If you set capture_pageview: false, emit them yourself from a usePathname() effect.
#4. Identify users
Call vt.identify(userId, properties) whenever the page loads for an authenticated user — not just on login. The most common integration bug is calling identify() only inside the login submit handler, which leaves returning logged-in visitors anonymous.
#Pattern A — server getCurrentUser() → prop
When the layout reads the session on the server, pass currentUser to a client provider. Do not call resetUser() when the prop is null — that visitor was never logged in. Reset in the logout handler only.
// app/providers/vtilt-provider.tsx
'use client'
import { useEffect, useRef } from 'react'
export function VTiltProvider({
currentUser,
children,
}: {
currentUser: { id: string; email: string; name?: string } | null
children: React.ReactNode
}) {
const identified = useRef<string | null>(null)
useEffect(() => {
const vt = window.vt
if (!vt || !currentUser?.id) return
if (identified.current === currentUser.id) return
vt.identify(currentUser.id, {
email: currentUser.email,
name: currentUser.name,
})
identified.current = currentUser.id
}, [currentUser?.id, currentUser?.email, currentUser?.name])
return <>{children}</>
}#Pattern B — client session hook
When auth comes from useSession(), Better Auth, NextAuth client, or Clerk, wait for loading === false before identify or reset:
'use client'
import { useEffect, useRef } from 'react'
import { useSession } from '@/lib/auth'
export function VTiltProvider({ children }: { children: React.ReactNode }) {
const identified = useRef<string | null>(null)
const { user, loading: sessionLoading } = useSession()
useEffect(() => {
if (sessionLoading) return
const vt = window.vt
if (!vt) return
if (user?.id) {
if (identified.current === user.id) return
vt.identify(user.id, { email: user.email })
identified.current = user.id
} else if (vt.getUserState() === 'identified') {
vt.resetUser()
identified.current = null
}
}, [user, sessionLoading])
return <>{children}</>
}#5. Server-side events with the Node SDK
For events from server actions, route handlers, and middleware, install the Node SDK:
npm install @v-tilt/nodeConstruct one client per process and await shutdown() before serverless functions exit:
// app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import { VTiltNode } from '@v-tilt/node'
const vtilt = new VTiltNode(process.env.VTILT_TRACKER_TOKEN!, {
host: process.env.NEXT_PUBLIC_VTILT_HOST,
})
export async function POST(req: Request) {
const { userId, amount } = await req.json()
vtilt.capture({
distinctId: userId,
event: 'purchase_completed',
properties: { amount, source: 'next-api' },
})
await vtilt.shutdown()
return NextResponse.json({ ok: true })
}#Next steps
- Identify & alias — the full identification model.
- Reverse proxy — route SDK traffic through your own domain.
- Node SDK / Capture, identify & alias — server-side event shapes.
- Event forwarding — fan events out to GA4, Meta CAPI, PostHog.
- Common mistakes — integration pitfalls to avoid before you ship.