Laravel Integration
Drop Lumio into any Laravel app — widget id in config, signing secret in .env, identity signed server-side in a Blade partial.
Laravel Integration
A copy-paste setup for any Laravel app (Blade, Livewire, or Inertia). The widget
id is public and lives in config; the signing secret stays in .env and the
identity HMAC is computed in Blade, so the secret never reaches the browser.
1. Configuration
Add your credentials to .env. The widget id is public (it ships in the page
HTML); the signing secret is sensitive — keep it out of version control.
LUMIO_WIDGET_ID=your-widget-id
LUMIO_SIGNING_SECRET=your-signing-secretBoth are shown on the widget's page in the Lumio dashboard (reveal the secret with Reveal signing secret, owner/admin only).
Expose them via config/services.php:
'lumio' => [
'widget_id' => env('LUMIO_WIDGET_ID'),
'signing_secret' => env('LUMIO_SIGNING_SECRET'),
],2. A Blade partial
Create resources/views/partials/lumio.blade.php. It renders nothing unless a
widget id is configured, and only signs an identity for authenticated users.
@if(config('services.lumio.widget_id'))
@php($lumioWidgetId = config('services.lumio.widget_id'))
@php($lumioSecret = config('services.lumio.signing_secret'))
<script>
window.LumioWidget = window.LumioWidget || function () { (window.LumioWidget.q = window.LumioWidget.q || []).push(arguments) }
</script>
<script src="https://lumio.teurons.com/widget/loader.v1.js" async></script>
<script>
LumioWidget('init', { widgetId: @js($lumioWidgetId) });
@auth
@if($lumioSecret)
LumioWidget('identify', {
userId: @js((string) auth()->id()),
signature: @js(hash_hmac('sha256', (string) auth()->id(), $lumioSecret)),
name: @js(auth()->user()->name ?? null),
email: @js(auth()->user()->email ?? null),
});
@endif
@endauth
</script>
@endifThe signature is HMAC-SHA256(userId, signingSecret) as lowercase hex — hash_hmac
runs server-side, so only the resulting signature is sent to the browser, never the
secret. See Identifying Users for the full contract.
Content-Security-Policy nonce
If your layout uses a CSP nonce (Vite::cspNonce()), add it to each <script> tag,
e.g. <script @if($cspNonce) nonce="{{ $cspNonce }}" @endif>. Make $cspNonce
available to the partial from your layout.
3. Include it in your layout
Add the partial to your main layout, before the closing </body> (or just after,
as the last thing on the page):
@include('partials.lumio')That's it. Authenticated users are now identified; anonymous visitors still get the widget (just without read-tracking or audience targeting).
4. Content-Security-Policy (if you have one)
If your app sends a CSP header, allow the Lumio origin. The widget loads its scripts,
an iframe, API calls, and an image proxy — all from https://lumio.teurons.com:
script-src ... https://lumio.teurons.com
frame-src ... https://lumio.teurons.com
connect-src ... https://lumio.teurons.com
img-src ... https://lumio.teurons.comIf you use a nonce-based policy with strict-dynamic, script-src is already
covered by the nonce on the loader tag — you only need frame-src, connect-src,
and img-src.
5. Verify identity is working
identify doesn't call your server — the widget's iframe posts to
lumio.teurons.com, so the request won't show under your own domain:
- Open DevTools → Network, tick Preserve log, select Fetch/XHR.
- Filter for
identify(orlumio.teurons.com). - Hard-reload the page.
- Look for
POST https://lumio.teurons.com/api/widget/<id>/identify:- 200 — identified. Working.
- 401 — signature mismatch (the secret in
.env≠ the widget's signing secret). - 403 — origin not allowed (add your domain to the widget's Allowed origins).
Troubleshooting
A few Laravel-specific gotchas, learned the hard way:
/embed loads but /identify never fires
The iframe is up but the identify block didn't render or wasn't sent. Check, in order:
- View source near
</body>— is theLumioWidget('identify', { … })block actually there with a realuserIdand a 64-charsignature? If it's missing, one of the Blade guards (@auth,@if($lumioSecret)) failed. - Empty
.envvalue silently wins over a config default.env('KEY', 'default')returns the default only when the key is absent — a present-but-emptyLUMIO_SIGNING_SECRET=resolves to"", not the default. Either set a real value or remove the line entirely. - Run
php artisan config:clearafter editing.env(and neverconfig:cachewith an empty/placeholder secret).
@auth is false even though the user is "logged in"
@auth and auth()->id() use the default auth guard (usually web). Multi-guard
apps (e.g. a separate web_verve / customer guard) won't match. Sign the identity
off the guard your users are actually on:
@php($lumioUser = auth('web_verve')->user())
@if($lumioUser)
LumioWidget('identify', {
userId: @js((string) $lumioUser->getAuthIdentifier()),
signature: @js(hash_hmac('sha256', (string) $lumioUser->getAuthIdentifier(), $lumioSecret)),
...
});
@endifInertia / SPA — identify only fires once
With Inertia (or any client-side router), the Blade layout — and therefore the
init/identify scripts — run only on a full page load. In-app navigations swap
content over XHR without re-running Blade, and the widget iframe persists. So
/identify fires once on the initial load, not on every route change. To keep
URL-based announcement targeting accurate, report each navigation:
LumioWidget('event', { type: 'page-viewed', url: window.location.href, title: document.title })Fire it from your router's afterEach hook (or Inertia's router.on('navigate', …)).