ZevAuth
Sign up

Your backend

Laravel

Protect routes, read the signed-in user with Auth::user(), use Blade directives and receive webhooks in Laravel.

composer require zevauth/zevauth-php
php artisan vendor:publish --tag=zevauth-config

Laravel 11 or later. The integration is part of the PHP library and is discovered automatically.

ZEVAUTH_ENVIRONMENT_ID=env_live_…
ZEVAUTH_PUBLISHABLE_KEY=pk_live_…
ZEVAUTH_SECRET_KEY=sk_live_…
ZEVAUTH_WEBHOOK_SECRET=whsec_…

Signing keys are cached in your default cache store, so they’re fetched about every ten minutes, not on every request. Set ZEVAUTH_CACHE_STORE to use another store.

Protecting routes

Route::middleware('zevauth')->group(function () {
    Route::get('/dashboard', DashboardController::class);
});

Route::middleware('zevauth:mfa')->post('/payouts', PayoutController::class);
Route::middleware('zevauth:allow-pending')->get('/account/setup', SetupController::class);
MiddlewareLets through
zevauthA signed-in session with no unfinished tasks
zevauth:mfaThe same, and the person has verified a second factor
zevauth:allow-pendingA signed-in session even with tasks to finish, for the pages that finish them

Combine options with a comma: zevauth:mfa,allow-pending. Refusals are JSON in the same shape as the ZevAuth API: 401 unauthorized, 403 session_task_pending, 403 mfa_required.

Who is signed in

use ZevAuth\Laravel\Facades\ZevAuth;

$auth = ZevAuth::auth();
$auth->userId;
$auth->has(permission: 'org:members:manage');

ZevAuth::requireAuth(requireMfa: true);   // throws AuthRefusedException

With Auth::user()

Point a guard at ZevAuth in config/auth.php:

'guards' => [
    'web' => ['driver' => 'zevauth'],
],

Auth::user(), $request->user(), @auth and Laravel’s own auth middleware then work. The user is built from the verified token, with no database query:

$user = $request->user();   // ZevAuth\Laravel\ZevAuthUser
$user->id;                  // user_…
$user->email;
$user->hasOrg(role: 'org:admin');

Keep your own records keyed by $user->id if you need them.

Blade

@zevauthSignedIn
    <p>Welcome back.</p>
@endzevauthSignedIn

@zevauthSignedOut
    <div id="sign-in"></div>
@endzevauthSignedOut

Server-rendered pages

Blade pages are requested by the browser itself, with no Authorization header. The integration registers the session bridge routes, POST /zevauth/session and GET /zevauth/handshake, so turning it on is one line in the browser:

<script src="https://cdn.jsdelivr.net/npm/@zevauth/ui@0/dist/zevauth-ui.global.js"></script>
<script>
  ZevAuthUI.init({
    publishableKey: '{{ config('zevauth.publishable_key') }}',
    sessionBridge: { url: '/zevauth/session' },
  });
  ZevAuthUI.mountSignIn('#sign-in');
</script>

From then on, the zevauth middleware, the guard and the Blade directives all see the session on page requests. When the cookie lapses, the middleware sends the browser through the handshake and straight back. The person never sees it.

The bridge cookies are kept out of Laravel’s cookie encryption automatically. Options are under bridge in config/zevauth.php: allowed_origins, cookie_domain to share the session with subdomains, and secure (turn it off only for plain-http local development).

Webhooks

// routes/web.php or routes/api.php
Route::zevauthWebhooks('/webhooks/zevauth', HandleZevAuthEvent::class);
final class HandleZevAuthEvent
{
    public function __invoke(Request $request)
    {
        $event = $request->attributes->get('zevauth.event');   // already verified

        if ($event['type'] === 'user.deleted') {
            User::where('zevauth_id', $event['data']['id'])->delete();
        }

        return response()->noContent();
    }
}

The route checks the signature against the raw body and refuses anything ZevAuth didn’t sign with a 400. It’s excluded from CSRF protection, since deliveries come from ZevAuth’s servers.

Backend API

$client = ZevAuth::client();   // built from ZEVAUTH_SECRET_KEY

$client->users->update($user->id, ['publicMetadata' => ['plan' => 'pro']]);
$client->organizations->addMember('org_…', userId: $user->id);

Everything on the PHP page applies: errors carry errorCode, status and requestId, and safe retries are automatic.

Octane

The guard and middleware read the current request on every call, so a long-lived Octane worker never carries one person’s session into the next request.

Updated at, Sunday, November 1, 2026