Docs
Docs
Guides

Laravel

Serve every customer domain from one Laravel app with a middleware that checks the Apexly signature and resolves the tenant.

How requests arrive

Apexly forwards each visitor's request to your origin with the original Host header, X-Forwarded-Proto: https, and X-Apexly-Domain-Id, X-Apexly-App-Id, X-Apexly-Timestamp and X-Apexly-Signature.

Verify the signature

X-Apexly-Signature is the hex HMAC-SHA256 of <timestamp>.<host> keyed with your signing secret (whsec_…). Put it in .env as APEXLY_SIGNING_SECRET and expose it as config('services.apexly.secret'). This function is tested against the shared vectors on every build.

<?php

/** True when X-Apexly-Signature is valid for this Host (the edge signs "{timestamp}.{host}"). */
function verify_apexly_origin(string $host, ?string $timestamp, ?string $signature, string $secret, ?int $now = null): bool
{
    if ($timestamp === null || $signature === null || !preg_match('/^\d{1,12}$/', $timestamp)) {
        return false;
    }
    if (abs(($now ?? time()) - (int) $timestamp) > 300) {
        return false;
    }
    $name = strtolower(preg_replace('/:\d+$/', '', $host));
    return hash_equals(hash_hmac('sha256', $timestamp . '.' . $name, $secret), $signature);
}

A middleware refuses unsigned requests for hosts you don't serve yourself and binds the tenant:

<?php
// app/Http/Middleware/ResolveTenant.php
namespace App\Http\Middleware;

use App\Models\Tenant;
use Closure;
use Illuminate\Http\Request;

class ResolveTenant
{
    private const OWN_HOSTS = ['app.hireflow.com'];

    public function handle(Request $request, Closure $next)
    {
        $host = $request->getHost();
        if (!in_array($host, self::OWN_HOSTS, true)) {
            $ok = verify_apexly_origin(
                $request->getHttpHost(),
                $request->header('X-Apexly-Timestamp'),
                $request->header('X-Apexly-Signature'),
                config('services.apexly.secret'),
            );
            abort_unless($ok, 421, 'Unknown host');
            app()->instance(Tenant::class, Tenant::where('hostname', $host)->firstOrFail());
        }
        return $next($request);
    }
}

Allow your customers' hostnames

Register the middleware first, and do not enable trustHosts (it rejects every host it does not list). Trust the forwarded protocol so url() and secure() produce https:// links:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->prepend(\App\Http\Middleware\ResolveTenant::class);
    $middleware->trustProxies(at: '*', headers: Request::HEADER_X_FORWARDED_PROTO);
})

Receive webhooks

Verify Apexly-Signature against $request->getContent() with verify_apexly_signature from the Webhooks page, and exclude the route from CSRF protection ($middleware->validateCsrfTokens(except: ['webhooks/apexly'])).

// routes/web.php
Route::post('/webhooks/apexly', function (Request $request) {
    $raw = $request->getContent();
    abort_unless(verify_apexly_signature($request->header('Apexly-Signature'), $raw, config('services.apexly.secret')), 400);
    $event = json_decode($raw, true);
    if ($event['type'] === 'domain.active') {
        Tenant::markLive($event['data']['domain']['metadata']['customer']);
    }
    return response()->noContent();
});

Try it

Run the Quickstart with a test key, then use the sandbox origin to see the headers your app will receive.

What to read next

Was this helpful?
Edit this page