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 / Identify users
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

API Reference

MCP server

On this page

On this page

  • The three moments to identify
  • Pattern: identify on every authenticated page load
  • 1. Render the current user into the page
  • 2. Identify after init, every page load
  • Real-world examples by stack
  • Next.js App Router — server user as prop
  • Next.js / React — client session hook
  • Laravel / Blade
  • Vue 3 + Pinia
  • 3. (Recommended) Identify on the server too, with the Node SDK
  • Pattern: login, signup and logout
  • Setting and updating user properties
  • Anonymous → identified merge
  • Opt-out, consent and reset
  • Pattern: Vue + PHP backend
DocsGetting StartedIdentify users

Identify users

Last updated July 22, 2026

Link events to authenticated users — at login, on every authenticated page load, and on logout. Includes the SSR pattern for already-logged-in visitors.

identify() is what turns vTilt from "anonymous traffic" into "people". An anonymous visitor browses with a generated $device_id. The first time you call identify('user-123'), the SDK emits a $identify event carrying both that anonymous id and the new user id; the backend merges the two person records and every later event is attributed to user-123.

Important

The single most common integration bug is calling identify() only on the login event. When a user lands on your site with an existing session cookie (the most common case), no login event fires — so identify() never runs, and that user stays anonymous until they log out and back in. Call identify() on every page load when a user is authenticated, not only at login. The identify itself is idempotent and safe to call repeatedly.

#The three moments to identify

MomentWhat to callWhere it lives in your app
Page load with a logged-in uservt.identify(currentUser.id, { … })Right after vt.init() on every page that has session context. This is the one most apps miss.
Login (or signup)vt.identify(newUser.id, { … })Your login success handler, before the redirect.
Logoutvt.resetUser()Logout handler, or after auth loads when getUserState() === 'identified' and user is now null — real sign-out only. Never on anonymous page loads.

Calling identify() for the same user id on every page load is correct and intended. The SDK shortcuts the network call when nothing has changed.

#Pattern: identify on every authenticated page load

The server already knows whether the request belongs to a logged-in user — your session cookie / auth token tells you. The cleanest pattern is to expose the current user as a tiny global on the rendered page, then have the SDK pick it up immediately after vt.init().

#1. Render the current user into the page

Server-render a small JSON blob into the HTML before any analytics code runs. Use whatever templating language fits your stack — examples:

<!-- Plain HTML / PHP / Twig / Rails / Django -->
<script>
  window.__currentUser = <?php echo json_encode(
      $currentUser
          ? [
              'id'    => $currentUser->id,
              'email' => $currentUser->email,
              'name'  => $currentUser->name,
              'plan'  => $currentUser->plan,
            ]
          : null,
  ); ?>;
</script>
html
<!-- Vue 3 with SSR / Nuxt -->
<script>
  window.__currentUser = {{ JSON.stringify(currentUser ?? null) }};
</script>
html
<!-- Next.js — pass via a server component to a small client island -->
<!-- See `pattern-nextjs` below for the recommended idiom. -->
html

#2. Identify after init, every page load

const user = window.__currentUser
if (user && user.id) {
  window.vt?.identify(user.id, {
    email: user.email,
    name: user.name,
    plan: user.plan,
  })
}
typescript

That's it. Anonymous visitors stay anonymous. Authenticated visitors are identified on their very first capture, every page load, regardless of whether a "login" happened in this tab or the cookie carried over from a previous visit.

For sign-out behaviour, see Logout & reset.

#Real-world examples by stack

Pick the block that matches how your app resolves auth. Do not mix server-rendered identify with client resetUser() on null unless you use a client session hook.

How the app knows the userWhere to identify()When to resetUser()
Server renders user on every HTML responseAfter vt.init() from window.__currentUser or a server-passed propLogout handler only — never in a reactive effect when user is null
Client session hook (useSession, Better Auth, Clerk, Supabase, …)useEffect after loading === falseloading === false and user is null and getUserState() === 'identified' (was logged in, now signed out)
Login handler only (anti-pattern)—Never rely on this alone

#Next.js App Router — server user as prop

// app/layout.tsx (server)
const currentUser = await getCurrentUser()
return (
  <body>
    {/* vTilt stub in <Script strategy="beforeInteractive"> */}
    <VTiltProvider currentUser={currentUser}>{children}</VTiltProvider>
  </body>
)
tsx
// 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

No resetUser() in this provider — call it in the logout handler when the user signs out.

#Next.js / React — client session hook

'use client'
import { useEffect, useRef } from 'react'
import { useSession } from '@/lib/auth' // or useUser(), authClient.useSession(), etc.

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, name: user.name })
      identified.current = user.id
    } else if (!sessionLoading && vt.getUserState() === 'identified') {
      vt.resetUser()
      identified.current = null
    }
  }, [user, sessionLoading])

  return <>{children}</>
}
tsx

#Laravel / Blade

<script>
  window.__currentUser = @json(auth()->check() ? [
    'id' => auth()->id(),
    'email' => auth()->user()->email,
  ] : null);
</script>
{{-- vTilt stub + vt.init() in <head> --}}
<script>
  (function () {
    var u = window.__currentUser;
    if (u && u.id) vt.identify(u.id, { email: u.email });
  })();
</script>
blade

#Vue 3 + Pinia

watch(
  () => [userStore.user, userStore.isReady] as const,
  ([user, isReady]) => {
    if (!isReady) return
    const vt = window.vt
    if (!vt) return
    if (user?.id) vt.identify(user.id, { email: user.email })
    else if (vt.getUserState() === 'identified') vt.resetUser()
  },
  { immediate: true },
)
typescript

Full stack guides: Next.js, React, Vue, Laravel, Django.

#3. (Recommended) Identify on the server too, with the Node SDK

For events that originate on the server (background jobs, webhooks, server-side conversion events) call identify() from the Node SDK with the same user id. The browser and Node sides converge on the same person record because they share the user id.

#Pattern: login, signup and logout

Add a single line to each auth handler. These calls augment — not replace — the per-page-load identify.

// On successful login — before the redirect
async function onLoginSuccess(user) {
  vt.identify(user.id, {
    email: user.email,
    name: user.name,
    plan: user.plan,
  })
  vt.capture('user_logged_in', { method: 'password' })
  // … redirect
}

// On signup — alias the freshly-created user with their pre-signup
// anonymous activity (otherwise the funnel before signup is orphaned).
async function onSignupSuccess(user) {
  vt.alias(user.id) // keeps the anonymous device id linked to the new user
  vt.identify(user.id, { email: user.email, plan: 'free' })
  vt.capture('user_signed_up', { method: 'email' })
}

// On logout — real sign-out; clears identity so the next person on a shared device isn't cross-attributed
function onLogoutSuccess() {
  window.vt?.capture('user_logged_out')
  window.vt?.resetUser()
  // … redirect
}
typescript

#Setting and updating user properties

Person properties (email, plan, role, country) are attached during identify() or independently via setUserProperties(). Both are merged onto the person record server-side; later calls overwrite earlier values for the same key.

vt.identify('user-123', {
  email: 'user@example.com',
  name: 'John Doe',
  plan: 'premium',
})

vt.setUserProperties({
  last_login: new Date().toISOString(),
  login_count: 5,
})
typescript

#Anonymous → identified merge

The first identify() call after a user authenticates is the merge. Internally:

  1. The SDK emits a $identify event with distinct_id = user.id and $anon_distinct_id = device_id.
  2. The backend creates (or updates) a person record for user.id and merges everything previously associated with the device id under that person.
  3. All subsequent events use distinct_id = user.id.

This means a visitor's pre-signup browsing — pageviews, autocapture events, performance metrics — is preserved on the same person once they sign up, as long as identify() runs in the same browser session before they leave.

#Opt-out, consent and reset

// Opt out of all analytics
vt.setConsent({ analytics: false })

// Opt back in
vt.setConsent({ analytics: true })

// Reset identity on logout (explicit sign-out handler)
vt.resetUser()
typescript

resetUser() clears the cached identity and generates a fresh anonymous $device_id on the next event. Use it only on real sign-out — in the logout handler, or in a reactive effect when auth has finished loading, the app user is null, and getUserState() === 'identified'. Do not call it for visitors who were never logged in.

#Pattern: Vue + PHP backend

For a complete worked example showing the SSR identify pattern in a Vue + PHP stack, see Vue + PHP.

For integration pitfalls, see Common mistakes.

PreviousInitializeGetting StartedNextTrack eventsGetting Started