On this page
Identify users
Last updated
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.
#The three moments to identify
| Moment | What to call | Where it lives in your app |
|---|---|---|
| Page load with a logged-in user | vt.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. |
| Logout | vt.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><!-- Vue 3 with SSR / Nuxt -->
<script>
window.__currentUser = {{ JSON.stringify(currentUser ?? null) }};
</script><!-- Next.js — pass via a server component to a small client island -->
<!-- See `pattern-nextjs` below for the recommended idiom. -->#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,
})
}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 user | Where to identify() | When to resetUser() |
|---|---|---|
| Server renders user on every HTML response | After vt.init() from window.__currentUser or a server-passed prop | Logout handler only — never in a reactive effect when user is null |
Client session hook (useSession, Better Auth, Clerk, Supabase, …) | useEffect after loading === false | loading === 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>
)// 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}</>
}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}</>
}#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>#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 },
)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
}#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,
})#Anonymous → identified merge
The first identify() call after a user authenticates is the merge. Internally:
- The SDK emits a
$identifyevent withdistinct_id = user.idand$anon_distinct_id = device_id. - The backend creates (or updates) a person record for
user.idand merges everything previously associated with the device id under that person. - 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()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.