Laravel Integration

bext runs Laravel inside the edge process. Unlike the Rails integration — which reverse-proxies to an external Puma — bext embeds libphp via a custom SAPI (crates/bext-php) and dispatches every request to a Laravel kernel held in the worker thread's memory. There is no php-fpm, no FastCGI socket, no subprocess. One binary serves TLS, HTTP/2/3, WAF, cache, and PHP.

Worker mode takes this further: the Laravel kernel boots once per worker thread at startup, and each subsequent request reuses that kernel through a bext_handle_request(...) loop. Per-request overhead drops from the classic 1–5 ms (Composer autoload + config rebuild + container wiring) to ~34 us (kernel reuse + GC cycle).

When to use this. If you already have a Laravel project and you want a single-binary edge + app server with zero ops footprint, this is the integration for you. If you want classic FastCGI with php-fpm, bext's reverse proxy can forward to it — but embedded is strictly faster and simpler.

Architecture

    ┌─────────┐          ┌───────────────────────────────────────┐
    │         │  HTTPS   │                bext                   │
    │ client  │ ───────► │  TLS • WAF • cache • obs • PHP kernel │
    │         │  :443    │                :8080                  │
    └─────────┘          └───────────────────────────────────────┘
                                        │
                                        ▼ (in-process, 34 us)
                               ┌──────────────────┐
                               │  Laravel kernel  │
                               │   (per worker)   │
                               └──────────────────┘

Request flow:

1. Client opens TLS to bext on :443 (or :8080 in dev). 2. bext terminates TLS, runs the WAF, checks the ISR cache, and routes the request. Static assets under /build/*, /storage/*, /favicon.ico, /robots.txt are served directly from disk without touching PHP. 3. Dynamic requests are dispatched into one of workers PHP worker threads. The worker's bext_handle_request(...) callback wakes up, Laravel's kernel handles the request, the response streams back to bext, and bext ships it to the client over HTTP/2 or HTTP/3.

bext.config.toml

The starter's config, stripped to the essentials:

[server]
port = 8080

[php]
enabled = true
document_root = "./public"
worker_script = "./bootstrap/worker.php"
workers = 4
max_requests = 10000
max_execution_time = 30

[php.ini]
memory_limit = "256M"
post_max_size = "16M"
upload_max_filesize = "16M"

[waf]
enabled = true

[[route_rules]]
pattern = "/build/*"
render = "static"

[[route_rules]]
pattern = "/storage/*"
render = "static"

Key fields:

  • worker_script — path to the PHP file that boots the kernel once per worker. When unset, bext falls back to classic mode (boots public/index.php per request). - workers — number of parallel kernels held in RAM. Each worker is a separate TSRM context; on NTS PHP this is clamped to 1. - max_requests — worker is recycled after this many requests to contain memory leaks in third-party packages. bext restarts it automatically with no dropped requests. - [php.ini] — any PHP INI key; bext sets sensible production defaults (OPcache on, opcache.validate_timestamps=0, disable_functions hardening, open_basedir sandboxing, JIT off by default).

See [php] reference for the full field list.

bootstrap/worker.php

The heart of worker mode. Runs once per thread at startup, then loops:

<?php
require __DIR__ . '/../vendor/autoload.php';

$app = require __DIR__ . '/app.php';
$kernel = $app->make(\Illuminate\Contracts\Http\Kernel::class);

while (bext_handle_request(function () use ($kernel) {
    $response = $kernel->handle(
        $request = \Illuminate\Http\Request::capture()
    );
    $response->send();
    $kernel->terminate($request, $response);
})) {
    gc_collect_cycles();
}

bext_handle_request is a host function exposed by bext-php. It blocks until a request arrives, invokes the callback, and returns true — unless the worker has hit max_requests, in which case it returns false and the while loop exits cleanly so bext can spawn a replacement worker.

What bext adds

Concern Who handles it
TLS / ACME bext
HTTP/2, HTTP/3 (QUIC) bext
WAF (bots, rate limit) bext
Edge cache (ISR / SWR) bext
Compression (brotli) bext
Prometheus metrics bext
OpenTelemetry traces bext
Static asset serving bext (see route rules above)
PHP runtime bext (embedded libphp SAPI)
Business logic Laravel
Eloquent / DB Laravel
Queues / jobs Laravel (Horizon, sync, database)
Artisan / migrations Laravel

Cache invalidation from Laravel

The companion bext/laravel Composer package registers helpers for invalidating bext's ISR cache from inside a controller:

class ProductController extends Controller
{
    public function update(Request $request, string $id)
    {
        $product = Product::findOrFail($id)->update($request->all());

        // Invalidate ISR cache entries tagged "products"
        bext_invalidate("products");

        return response()->json($product);
    }
}

See the full bext/laravel helper reference below.

Helpers and services

Install the companion Composer package to unlock cache, real-time, and task helpers:

composer require bext/laravel

Three globals are then available everywhere:

bext_invalidate("products");
bext_publish("orders", ["id" => 123, "status" => "shipped"]);
bext_task("cleanup", "0 2 * * *", "cache::gc");

Or inject from the container:

app('bext.cache')->invalidate("products");
app('bext.realtime')->publish("orders", [...]);
app('bext.tasks')->register("daily-report", "0 9 * * 1-5", $url);

Detection

bext's infra import scanner recognises a Laravel project from:

- a composer.json that declares laravel/framework, and

- an artisan CLI file

When both are present, bext infra import suggests the Laravel integration pattern and offers to scaffold from @bext/starter-laravel.

The catalog entry lives in crates/bext-import/catalog/frameworks.yaml:

- match:
    any_of_files:
      - "composer.json"
      - "artisan"
    composer:
      any_of_packages:
        - "laravel/framework"
  extract:
    framework: laravel
    starter: "@bext/starter-laravel"
  confidence: high

The starter

bext new my-laravel-app --template @bext/starter-laravel
cd my-laravel-app
composer install
bext serve

Visit http://localhost:8080.

The starter is a scaffold, not a working Laravel app. It ships the minimum shape bext needs: a composer.json, a front controller, a bootstrap/worker.php, a tiny HomeController and web.php route, and a bext.config.toml with the [php] block wired up. Use composer create-project laravel/laravel to generate a real Laravel project and drop the starter's bext.config.toml + bootstrap/worker.php into it — that is the canonical workflow.

How this compares to Rails

Dimension bext + Rails bext + Laravel
Process model bext + separate Puma bext only (embedded PHP)
Inter-process hop HTTP/1.1 loopback none — in-process call
Per-request overhead ~100–300 us (proxy) ~34 us (worker mode)
Runtime language Ruby (external interpreter) PHP (embedded libphp)
Config section [upstreams.*] + proxy = [php]
systemd units 2 (bext + rails-app.service) 1 (bext)
Deploy artifact Gemfile + app tree + bext composer + app tree + bext

The asymmetry exists because the PHP and Ruby ecosystems ship very different embedding stories. libphp exposes a stable SAPI that dozens of web servers embed; Ruby does not. Rails under Puma is fast and ergonomic, so the HTTP reverse-proxy path is the right trade-off for Rails. PHP embedded in-process is strictly faster and simpler for Laravel.

Limitations

  • Worker mode requires ZTS PHP. Non-thread-safe (NTS) builds of PHP fall back to classic mode (no persistent kernel). Build PHP with --enable-zts to unlock worker mode — or use bext's pre-built PHP distribution which ships ZTS by default. - No process isolation between requests. Because requests share the worker's kernel, a crash in one request crashes the whole worker. bext automatically restarts the worker and replays in-flight requests, but this is not as bulletproof as php-fpm's per-request process model. max_requests helps contain slow leaks. - First boot is slow. Booting the Laravel kernel is a 100–300ms operation. bext boots all workers concurrently at startup, so the first request after bext serve may hit cold workers until they all come up. - Extensions must be bundled at build time. bext-php links against a curated extension set (pdo_mysql, pdo_pgsql, redis, gd, mbstring, intl, opcache). Custom extensions require a rebuild of bext — they cannot be loaded at runtime.

Related

- @bext/starter-laravel — the scaffold

- Embedded PHP runtime[php] section reference

- Auto TLS — ACME-based certificates

- WAF & Security — edge protections

- Rails Integration — reverse-proxy counterpart