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 / Vue + PHP
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
Vue + PHP

API Reference

MCP server

On this page

On this page

  • Stack assumptions
  • 1. Render the current user into the page
  • Laravel Blade
  • Symfony Twig
  • Plain PHP
  • 2. Initialise vTilt once, in your Vue entry
  • 3. Track SPA route changes
  • 4. Login, signup and logout flows
  • 5. (Recommended) Server-side events from PHP
  • 6. Verify the integration
  • Checklist
  • Next steps
DocsGuidesStack guidesVue + PHP

Vue + PHP

Last updated July 22, 2026

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.

Important

If identify() is called only inside your Login.vue submit handler, every visitor who arrives with an existing session cookie stays anonymous in vTilt. The fix is to identify on every authenticated page load, not just at login.

#Stack assumptions

  • PHP back-end renders the HTML shell and exposes $currentUser from the session (Laravel auth()->user(), Symfony Security::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 is https://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>
blade

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

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

Note

Always cast user ids to strings when serialising. JavaScript doesn't safely round-trip integers larger than 2^53. If your ids are auto-increment integers this will eventually bite you.

#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')
typescript

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

Tip

With the script-tag snippet (no bundler), the SDK monkey-patches pushState/replaceState and listens for popstate, so SPA route changes work automatically. The manual afterEach above is needed only when you bypass the standard History API in a custom router.

#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>
vue
<!-- 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>
vue
<!-- 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>
vue

#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,
        ],
    ],
);
php

Tip

Run server-side captures inside an after-response queue (Laravel's dispatch(), Symfony Messenger) so analytics never blocks the user-facing response. Network failures shouldn't surface to your users.

#6. Verify the integration

  1. Open your app in an incognito window — you're now an anonymous visitor.
  2. Open the Debug View in another tab. You should see a $pageview event arrive within ~1 second.
  3. Sign in. The next event should be $identify with your user id, followed by user_logged_in.
  4. Refresh the page while signed in. You should see another $identify immediately — this is the per-page-load identify confirming the integration. The Debug View shows it as a no-op merge against the same id.
  5. 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 = null even 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 under vt.init() in main.ts.
  • The user object exists but user.id is missing. Always send the canonical user id, never the email.

#Checklist

  • PHP renders window.__vtiltContext with { 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 when user is non-null.
  • Login / signup handlers call vt.identify (and vt.alias on signup).
  • Logout handler calls vt.resetUser.
  • SPA navigations (if any) emit $pageview from the router's afterEach.
  • 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.

#Next steps

  • Common mistakes — integration pitfalls to avoid before you ship.
PreviousRustIntegration guidesNextScript bundlesBrowser SDK