Lumio

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-secret

Both 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>
@endif

The 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.com

If 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:

  1. Open DevTools → Network, tick Preserve log, select Fetch/XHR.
  2. Filter for identify (or lumio.teurons.com).
  3. Hard-reload the page.
  4. 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 the LumioWidget('identify', { … }) block actually there with a real userId and a 64-char signature? If it's missing, one of the Blade guards (@auth, @if($lumioSecret)) failed.
  • Empty .env value silently wins over a config default. env('KEY', 'default') returns the default only when the key is absent — a present-but-empty LUMIO_SIGNING_SECRET= resolves to "", not the default. Either set a real value or remove the line entirely.
  • Run php artisan config:clear after editing .env (and never config:cache with 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)),
        ...
    });
@endif

Inertia / 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', …)).

On this page