vTilt
By roleFoundersKnow what to fix firstMarketersSee what happens after the clickCMOsSee what your budget really buys
By businessSaaSTurn signups into active usersEcommerceSee why shoppers abandon cartsAgenciesProve your work to clientsEnterpriseOne customer record for every team
MeasureWeb AnalyticsFind where buyers leaveSession ReplaySee why they left, not just that they didAdsKnow what paid visitors do nextIntegrationsOne snippet feeds your stack
ThinkPeopleOne profile per customerCustomer MemoryvTilt remembers every visitorSite KnowledgeAnswers and audits from your own pages
ActAsk AIAnswers from real behaviorAI ChatChat that knows the visitorEmail CampaignsEmail from real visits
PricingWhy vTiltDocs
Docs / Next.js
Getting Started
OverviewInstallInitializeIdentify usersTrack eventsLogout & resetVerify eventsCommon mistakes
Guides
Event forwardingReverse proxyRealtime dashboardSite knowledge
Frontend frameworks
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus
Backend frameworks
NestJSHonoCloudflare WorkersDjangoFlaskLaravelPhoenixRuby on Rails
Backend languages
PythonPHPRubyElixirGoJava.NET / C#Rust
Stack guides
Vue + PHP
API Reference
Browser SDK
Script bundlesAutocaptureWeb VitalsSession recordingChat widgetFeature readinessRemote configurationDebug logging
Node SDK
Install & setupCapture, identify & aliasContext & shutdownGlobal properties & opt-outError tracking
MCP server
Guides
OverviewAuthenticationOAuthAgent skills (prompts)AI intelligenceSite knowledgeGoogle AdsMeta Ads
Client setup
CursorClaude DesktopVS CodeCodex

Getting Started

OverviewInstallInitializeIdentify usersTrack eventsLogout & resetVerify eventsCommon mistakes

Guides

Event forwardingReverse proxyRealtime dashboardSite knowledge
Next.jsNuxt.jsVue.jsReactReact RouterRemixGatsbySvelte / SvelteKitAstroAngularTanStack StartDocusaurus

API Reference

MCP server

On this page

On this page

  • 1. Add your environment variables
  • 2. Install & initialise
  • 3. Track route changes
  • 4. Identify users
  • Pattern A — server getCurrentUser() → prop
  • Pattern B — client session hook
  • 5. Server-side events with the Node SDK
  • Next steps
DocsGuidesFrontend frameworksNext.js

Next.js

Last updated July 22, 2026

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.

Tip

New to vTilt? Skim the Quick start first — this guide assumes you have a project token.

#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
text

#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>
  )
}
tsx

For the Pages Router, put the same <Script> in pages/_app.tsx.

Note

With the inline script, vt is already on window — call it from any client component without importing @v-tilt/browser.

Tip

Need chat on first paint? Swap array.js for array.chat.js in the stub (or import the chat bundle on npm) and add chat: { enabled: true } to the init config. See Script bundles.

#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}</>
}
tsx

#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}</>
}
tsx

Warning

Calling resetUser() whenever user is null before sessionLoading finishes clears anonymous identity on every page load. Wait for loading to finish, then reset only when user is null and getUserState() === 'identified' (real sign-out).

#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/node
bash

Construct 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 })
}
typescript

Note

On serverless platforms (Vercel, Netlify, Cloudflare), call await vtilt.shutdown() before each handler returns to flush events before the function freezes.

#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.
PreviousSite knowledgeGuidesNextNuxt.jsIntegration guides