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 (bootspublic/index.phpper 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_basedirsandboxing, 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-ztsto 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_requestshelps 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 afterbext servemay 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