Vue + PHP
End-to-end vTilt integration for Vue front-end + PHP back-end (Laravel, Symfony, plain PHP), including the identify-on-every-page-load pattern.
This guide shows how to integrate vTilt into a Vue front-end with a PHP back-end (Laravel, Symfony, or plain PHP). It covers the pattern most apps with a server-rendered shell get wrong: identifying users who are already logged in when the page loads.
#Stack assumptions
- PHP back-end renders the HTML shell and exposes
$currentUserfrom the session (Laravelauth()->user(), SymfonySecurity::getUser(), or whatever your app uses). - Vue 3 owns the rest of the UI. Could be Inertia, Vue Router (SPA), or progressively-enhanced multi-page (Blade + Vue islands) — the integration shape is the same.
- vTilt project token is
YOUR_PROJECT_TOKEN. Your reverse proxy or vTilt host ishttps://www.vtilt.com.
#1. Render the current user into the page
The server already knows who the user is — pass that down once, in the HTML shell, before Vue boots. The exact template syntax differs by framework but the output is the same: a single global containing either the user object or null.
#Laravel Blade
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
{{-- ... --}}
<script>
window.__vtiltContext = {
token: @json(config('services.vtilt.token')),
apiHost: @json(config('services.vtilt.host')),
user: @json(auth()->check() ? [
'id' => (string) auth()->id(),
'email' => auth()->user()->email,
'name' => auth()->user()->name,
'plan' => auth()->user()->plan,
] : null),
};
</script>
</head>
<body>
<div id="app"></div>
@vite(['resources/js/app.js'])
</body>
</html>#Symfony Twig
{# templates/base.html.twig #}
<script>
window.__vtiltContext = {
token: {{ vtilt_token|json_encode|raw }},
apiHost: {{ vtilt_host|json_encode|raw }},
user: {{ (app.user
? { id: app.user.id|json_encode, email: app.user.email, name: app.user.name, plan: app.user.plan }
: null)|json_encode|raw }},
};
</script>#Plain PHP
<script>
window.__vtiltContext = <?= json_encode([
'token' => $vtiltToken,
'apiHost' => $vtiltHost,
'user' => $currentUser
? [
'id' => (string) $currentUser->id,
'email' => $currentUser->email,
'name' => $currentUser->name,
'plan' => $currentUser->plan,
]
: null,
], JSON_THROW_ON_ERROR) ?>;
</script>#2. Initialise vTilt once, in your Vue entry
In your main.js / main.ts (or whichever file boots Vue), import and init the SDK before mounting the app. Then identify the user from the context blob.
// resources/js/main.ts
import { createApp } from 'vue'
import { vt } from '@v-tilt/browser'
import App from './App.vue'
import { router } from './router' // optional, if SPA
import { installVtilt } from './vtilt'
const ctx = window.__vtiltContext
vt.init(ctx.token, {
api_host: ctx.apiHost,
autocapture: true,
capture_pageview: true,
capture_pageleave: true,
})
if (ctx.user) {
vt.identify(ctx.user.id, {
email: ctx.user.email,
name: ctx.user.name,
plan: ctx.user.plan,
})
}
const app = createApp(App)
installVtilt(app, router)
app.mount('#app')Now every authenticated visitor is identified on first paint, on every page load. Anonymous visitors stay anonymous until they sign up or log in.
#3. Track SPA route changes
If you use Vue Router for client-side navigation, the SDK doesn't see those automatically — it only sees the initial server-rendered URL. Hook the router and emit a pageview on every successful navigation.
// resources/js/vtilt.ts
import type { App } from 'vue'
import type { Router } from 'vue-router'
import { vt } from '@v-tilt/browser'
export function installVtilt(app: App, router?: Router) {
if (router) {
router.afterEach((to, from, failure) => {
if (failure) return
vt.capture('$pageview', {
$current_url: window.location.href,
$pathname: to.fullPath,
$referrer: from.fullPath,
})
})
}
// Expose `vt` to templates / `useVt()` composables if you want
app.config.globalProperties.$vt = vt
app.provide('vt', vt)
}#4. Login, signup and logout flows
Augment your auth handlers so client-side state updates also tell vTilt. The page-load identify already covers most cases — these calls handle the moments where the page-load context isn't fresh yet.
<!-- resources/js/Pages/Login.vue -->
<script setup lang="ts">
import { vt } from '@v-tilt/browser'
async function onSubmit(form) {
const res = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(form),
headers: { 'Content-Type': 'application/json' },
})
const { user } = await res.json()
vt.identify(user.id, {
email: user.email,
name: user.name,
plan: user.plan,
})
vt.capture('user_logged_in', { method: 'password' })
window.location.href = '/dashboard'
}
</script><!-- resources/js/Pages/Signup.vue -->
<script setup lang="ts">
import { vt } from '@v-tilt/browser'
async function onSubmit(form) {
const res = await fetch('/api/auth/signup', {
method: 'POST',
body: JSON.stringify(form),
headers: { 'Content-Type': 'application/json' },
})
const { user } = await res.json()
// alias() preserves the user's pre-signup browsing in their new person record
vt.alias(user.id)
vt.identify(user.id, { email: user.email, plan: 'free' })
vt.capture('user_signed_up', { method: 'email' })
window.location.href = '/onboarding'
}
</script><!-- LogoutButton.vue -->
<script setup lang="ts">
import { vt } from '@v-tilt/browser'
async function onLogout() {
vt.capture('user_logged_out')
vt.resetUser()
await fetch('/api/auth/logout', { method: 'POST' })
window.location.href = '/'
}
</script>#5. (Recommended) Server-side events from PHP
Some events are best emitted from PHP — webhook handlers, scheduled jobs, server-side conversions, or any place a redirect kills the browser session before the SDK can flush. Use a tiny HTTP helper since there's no official PHP SDK; the wire format is identical to @v-tilt/node.
function vtilt_capture(string $token, string $apiHost, array $event): void
{
$payload = [
'api_key' => $token,
'event' => $event['event'],
'distinct_id' => $event['distinctId'],
'properties' => $event['properties'] ?? [],
'timestamp' => $event['timestamp'] ?? gmdate('c'),
];
$ch = curl_init("$apiHost/api/e");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 2,
]);
curl_exec($ch);
curl_close($ch);
}
vtilt_capture(
config('services.vtilt.token'),
config('services.vtilt.host'),
[
'event' => 'subscription_renewed',
'distinctId' => (string) $user->id,
'properties' => [
'plan' => $user->plan,
'amount' => 99.0,
],
],
);#6. Verify the integration
- Open your app in an incognito window — you're now an anonymous visitor.
- Open the Debug View in another tab. You should see a
$pageviewevent arrive within ~1 second. - Sign in. The next event should be
$identifywith your user id, followed byuser_logged_in. - Refresh the page while signed in. You should see another
$identifyimmediately — this is the per-page-load identify confirming the integration. The Debug View shows it as a no-op merge against the same id. - Click "Logout". You should see
user_logged_out, then later events should drop back to a fresh anonymous$device_id.
If step 4 doesn't fire, your page-load identify isn't running. Common causes:
- The PHP template renders
__vtiltContext.user = nulleven when the user is authenticated. Check your auth helper. - The
identify()call is wrapped in a Vue lifecycle hook that runs before init; move it directly undervt.init()inmain.ts. - The user object exists but
user.idis missing. Always send the canonical user id, never the email.
#Checklist
- PHP renders
window.__vtiltContextwith{ token, apiHost, user }in the shell template. - Vue entry calls
vt.init(...)once with the token from context. - Vue entry calls
vt.identify(user.id, ...)immediately after init whenuseris non-null. - Login / signup handlers call
vt.identify(andvt.aliason signup). - Logout handler calls
vt.resetUser. - SPA navigations (if any) emit
$pageviewfrom the router'safterEach. - Server-side events are sent over HTTP from PHP for the moments the browser misses.
Once these are in place, the Persons view in the dashboard reflects real authenticated users, not orphaned anonymous device ids.