Releases: Qbix/webserver
Release list
v2.1.0 – Strange New Worlds
Qbix Server v2.1.0 — Strange New Worlds
v2.0 was a mesh-networked runtime. v2.1 makes it production-ready: every major PHP framework runs unmodified, the control panel manages apps end-to-end, mobile apps can be built and distributed for iOS and Android from the panel, and nine security audit passes harden the entire stack.
The mesh layer is still there — every v2.0 feature works unchanged. v2.1 adds the framework compat layer, a rebuilt control panel, mobile build scaffolding, and a significant number of security fixes.
Framework Compatibility
The compat source transform now handles output buffer protection, SAPI detection, and 27 shimmed PHP functions. Frameworks like Laravel, Symfony, WordPress, Drupal, CakePHP, Yii, and Mezzio run out of the box — no code changes, no plugins, no extensions.
# Drop a Laravel app in and go
php qbixserver.php --root=my-laravel-app/public --port=8080Source Transform
The rewriter intercepts calls that would break under Qbix's execution model:
ob_end_flush(),ob_end_clean(),ob_get_clean(),ob_get_level()are shimmed so frameworks that drain output buffers in a loop don't infinite-loop against Qbix's protected buffer.php_sapi_name()returns'cli-server'instead of'cli'. ThePHP_SAPIconstant is replaced via a context-aware constant engine that skips qualified references likeSomeClass::PHP_SAPI.- All shimmed calls are emitted fully qualified (
\Q_WebServer_Compat::_header(...)) so they resolve correctly inside namespaced framework code. The v2.0 rewriter omitted the leading backslash, which broke every Symfony, Laravel, and Drupal response.
13 Framework Presets
Built-in presets for Laravel, Symfony, WordPress, Drupal, CakePHP, CodeIgniter, Yii, Mezzio, Slim, Joomla, Magento, Nextcloud, and ownCloud. Each preset sets the front controller, upload limits, memory limits, and session settings appropriate for the framework.
12 Boot Adapters
Boot adapters automatically detect and bootstrap each framework. A Custom adapter allows user-specified boot callables.
Performance
Benchmarked against PHP's built-in development server:
| Framework | php -S | Qbix Server | Speedup |
|---|---|---|---|
| Laravel | 190 req/s | 653 req/s | 3.4× |
| Drupal | 404 req/s | 1,636 req/s | 4.0× |
| CakePHP | 905 req/s | 1,790 req/s | 2.0× |
| Mezzio | 1,580 req/s | 2,337 req/s | 1.5× |
See BENCHMARKS.md for full methodology.
Control Panel
App Management
Create, delete, start, stop, and configure apps from the panel. Domain assignment, SSL cert provisioning, and framework detection are integrated into the app creation flow. The panel also manages WordPress and Drupal plugin/module installation through their respective CLIs.
Cache Clearing
Append ?Q.clearCache to any URL to clear all caches — opcode, static file, precompression, and image — for that app. During development this replaces the cycle of clearing APC, restarting workers, and purging the image cache separately.
Script Modules
The panel's JavaScript is organized into modules with a loader, replacing the inline scripts that grew to several thousand lines in v2.0. The module system loads only the code needed for the active tab.
Mobile Build and Distribution
The control panel can scaffold, build, and package native iOS and Android apps.
Scaffolding
Click Prepare with a platform selected and the panel generates a complete native project:
- iOS: Swift source files +
project.ymlfor xcodegen.PhpBridge.swiftstarts PHP viaposix_spawn,TransportManager.swifthandles BLE/MC/LAN,BackgroundKeepAlive.swiftmaintains the silent audio session. - Android: Kotlin source files + Gradle (Kotlin DSL).
PhpBridge.ktextracts the binary from APK assets and runs it viaProcessBuilder,TransportManager.kthandles BLE/LAN,QbixServerService.ktruns the Foreground Service.
Both include the GATT service UUIDs, chunking protocol, and mesh handshake integration.
Building
Click Build and the panel runs xcodebuild or ./gradlew assembleRelease. CI produces qbixserver-ios-arm64 and qbixserver-android-arm64 micro binaries via static-php-cli, with Android NDK cross-compilation.
Distribution
iOS apps are archived and uploaded to App Store Connect via the Xcode Organizer or xcodebuild -exportArchive. TestFlight handles beta distribution (25 internal testers without review, 10,000 external testers with review). Ad Hoc provisioning supports direct distribution to up to 100 registered devices.
Android apps are signed with a release keystore and uploaded to the Google Play Console as an AAB. The Play Console offers internal (100 testers, no review), closed (invite-only), and open testing tracks before production. Signed APKs can also be distributed directly for sideloading or alternative stores.
See mobile/iOS.md and mobile/Android.md for full walkthroughs.
Security Hardening
Nine audit passes reviewed Panel.php (~7,000 lines), TransportManager.swift (~860 lines), and TransportManager.kt (~680 lines). Each pass found progressively fewer issues — 7, then 3, then 1 — confirming convergence.
XSS Prevention
All innerHTML assignments in the Nearby tab now escape API-returned data through escHtml(). Previously, a peer could set its mesh name to <img onerror=...> and have it rendered unsanitized in the control panel. Fixed in: mesh identity display, routing table rows, peer connection status, script selector options, attestation labels, and attestation signer fields.
Path Traversal
basename() is applied to directory names from user input in apiQbixNpm, apiFrameworkPkgDownload, and framework package endpoints. Without this, a target value of ../../etc could traverse outside the expected directory.
Shell Injection
escapeshellarg() is applied to all user-supplied paths passed to shell commands. This covers the WordPress CLI path (wp) and Drupal CLI path (drush) in six locations across apiFrameworkPackages, apiFrameworkPkgAction, and apiFrameworkRun. A crafted CLI path like /usr/bin/wp; rm -rf / would previously execute the injected command.
Authentication
apiChangePassword and apiLogout now check the Authorization: Bearer header in addition to X-Panel-Token and cookies, matching the existing checkAuth logic. API clients authenticating via Bearer header could previously not change their password or log out — the token lookup returned empty and the operation silently failed or invalidated the wrong session.
BLE SSRF
Both the iOS and Android TransportManagers now reject BLE-received HTTP request paths that don't start with /. Without this check, a crafted BLE request could use the path as a userinfo@host trick — the HTTP client would interpret user:pass@evil.com/path as a request to evil.com — turning the local server into an open proxy reachable over Bluetooth.
// iOS fix
guard parsed.path.hasPrefix("/") else {
sendBLEResponse("HTTP/1.1 400 Bad Request\r\n..."
.data(using: .utf8)!, to: central)
return
}// Android fix
if (!path.startsWith("/")) return@submitMultipeerConnectivity Peer Identity
The iOS TransportManager now uses the peer's actual mesh_id from the ECDH handshake when relaying MultipeerConnectivity messages to PHP, instead of the MC displayName. The MC display name is an arbitrary string set by the remote device; the mesh_id is the cryptographic identity established during the handshake. Using the display name meant the PHP server couldn't match MC messages to the correct peer, breaking message routing for any peer whose display name didn't happen to match its mesh ID.
Documentation
| Doc | What's new |
|---|---|
| FRAMEWORKS.md | All 13 frameworks with presets, adapters, benchmarks |
| BENCHMARKS.md | Framework benchmark section |
| mobile/README.md | Transport layer, GATT protocol, platform requirements |
| mobile/iOS.md | Signing, TestFlight, App Store, Ad Hoc distribution |
| mobile/Android.md | Keystore, Play Store, testing tracks, direct APK |
| README.md | Expanded Mobile section with build/distribute overview |
Upgrading from v2.0
No breaking changes. All v2.0 configuration, APIs, and mesh behavior are preserved. The framework compat layer activates only when serving a framework that needs it. The security fixes apply automatically.
v2.0.0 – Strange New Worlds
Qbix Server v2.0.0 — Strange New Worlds
v1.x was a PHP web server. v2.0 is a mesh-networked runtime. Phones discover each other over Bluetooth, establish encrypted sessions without any central server, synchronize data peer-to-peer, and run PHP applications on every node.
The web server is still there — every v1.x feature works unchanged. v2.0 adds a new capability surface on top.
Mesh Protocol
Every Qbix Server instance has a cryptographic identity: an ECDSA P-256 keypair generated on first run. The peer ID is sha256(public_key) — 64 hex characters, same security model as Ethereum. No registration, no CA, no blockchain.
When two servers discover each other (over BLE, Wi-Fi, or TCP), they perform a 4-step handshake: certificate exchange, mutual authentication via ECDSA signatures, ECDH ephemeral key agreement, and AES-256-GCM session encryption. All subsequent traffic is encrypted end-to-end.
// Any Qbix app can now talk to nearby servers
Q::handleUsingRemote('qbix-peer://' . $peerId . '/api/endpoint', $data);
// React to peers coming and going
Q_WebServer_Transport::onPeerOnline(function ($peer) {
// $peer has: peer_id, name, transport, address
// Sync data, exchange messages, coordinate work
});Routing
When peers are out of direct range, intermediate nodes relay traffic. The router uses distance-vector routing (the same algorithm that ran the early internet) with HELLO/BYE/HEARTBEAT messages, TTL limits, deduplication, and route dampening. A message from A to C routes through B transparently — encrypted end-to-end so B can't read it.
Data Sync
When a peer connects, the sync protocol runs automatically:
- Exchange Bloom filters (1KB each) to identify what's different
- Transfer only the missing records
- Resolve conflicts (last-writer-wins by default, pluggable per table)
For large datasets (10,000+ records), the protocol switches to prolly tree comparison: a deterministic content-addressed tree where identical subtrees are skipped entirely. With 10,000 records and 100 differences, the diff takes 1.2ms and transfers ~11KB of hashes instead of the full dataset.
Mobile Platforms
Qbix Server runs on iOS and Android. PHP runtimes now exist for both platforms (NativePHP Mobile, php-ios, Phphone). The native TransportManager handles peer discovery and transport negotiation.
Transport Priority
When a peer is reachable over multiple channels, the best one is used automatically:
| Priority | Transport | Bandwidth | Range |
|---|---|---|---|
| 1 | TCP (LAN/Wi-Fi) | 100+ Mbps | Same network |
| 2 | MultipeerConnectivity | 2–25 Mbps | ~200ft (iOS only) |
| 3 | BLE GATT | ~2 Mbps | ~100ft per hop |
If Wi-Fi drops, traffic falls back to BLE seamlessly. The PHP server never knows — it sees HTTP on localhost regardless of transport.
BLE Chunking
HTTP payloads are chunked for BLE's MTU constraints (23–517 bytes per write). The chunking protocol is implemented identically in PHP, Swift, and Kotlin — flag byte + 4-byte length header, tested against MTU 23 (BLE 4.0), 247 (typical), and 517 (BLE 5.0 max). Maximum message size: 64KB over GATT.
Background Persistence
- iOS: silent AVAudioEngine with
MixWithOtherskeeps the process alive. App Store precedent: PocketServer, Mob framework. - Android: Foreground Service with persistent notification.
START_STICKYfor auto-restart.
New in the Control Panel
Tab 14 ("Nearby") shows:
- This server's mesh identity (peer_id)
- Active transports (TCP, BLE, MultipeerConnectivity)
- Connected peers with transport type, hop count, encryption status
- Routing table (destination, next hop, hops)
- Encrypted sessions list
- Connect button for manually adding a TCP peer by address
Test Summary
| Suite | Tests |
|---|---|
| Mesh identity + handshake | 37 |
| Transport registry + events | 52 |
| Encrypted P2P HTTP | 39 |
| Routing + multi-hop | 65 |
| Bloom filter sync | 59 |
| BLE transport simulation | 63 |
| Prolly tree sync | 41 |
| Integration (all wired) | 37 |
| Server (HTTP, panel, workers) | 71 |
| Total | 464 |
Source Files (Mesh Layer)
| File | Lines | Purpose |
|---|---|---|
| Mesh.php | 718 | Identity, handshake, ECDH, AES-256-GCM |
| Transport.php | 692 | Peer registry, events, API, router wiring |
| MeshRouter.php | 537 | Distance-vector routing, FORWARD relay |
| MeshSync.php | 450 | Bloom filters, conflict resolution, sync flow |
| MeshBLE.php | 332 | Chunking protocol, rate limiter, simulator |
| ProllyTree.php | 448 | Content-addressed tree, O(d·log n) diff |
| TransportManager.swift | 844 | iOS: LAN + MC + BLE, auto-fallback |
| TransportManager.kt | 658 | Android: LAN + BLE, same protocol |
Upgrading from v1.x
No breaking changes. All v1.x configuration, APIs, and behavior are preserved. The mesh layer is additive — it activates when peers are discovered and does nothing when running as a standalone server.
The mesh identity keypair is generated automatically on first run and stored in the data directory. Deleting it generates a new identity (same as losing an Ethereum wallet).
What Was in v1.5
Everything from v1.5 is included: ECDSA M-of-N signing, Sigstore Rekor transparency log, SQLite auto-provisioning, PHP 8.6 Io\Poll epoll driver, Metrics/Analytics with clickstream, autohost for 15 frameworks, 4-platform CI, --pack single-binary mode, Windows COW fork.
Fixes in this build
- Framework code in namespaces. The source rewriter emitted
Q_WebServer_Compat::_header(...)without a leading backslash, so inside anynamespaceblock PHP looked forVendor\Namespace\Q_WebServer_Compatand the request failed with "class not found". Every Symfony, Laravel and Drupal response goes through a namespacedResponse::sendHeaders(), so all three returned 500. Shims are now emitted fully qualified. Regression test:tests/test_compat_namespace.php. /Q/sync/*and/Q/api/transport/*returned 500 for every request. The raw query string was passed toarray_merge(), which throws on a string. These are the endpoints the native TransportManager and remote peers call, so no peer could connect over HTTP.- Peer requests hung. A server receiving an encrypted request from a peer made an HTTP call back to its own port from inside its event loop, which could never be answered, and used an undeclared
$listenPortthat always resolved to 8080. Requests are now dispatched in-process through the router. - A proxied peer response could crash the server. API results whose
statusfield was a string ("ok"from a peer's/Q/health) were used as the HTTP status code, and the metrics recorder then failed on arithmetic with a string. API status codes are now validated and metrics casts to int. - Bloom filters saturated. Sync filters were a fixed 1KB, which gave a 93% false-positive rate at 5,000 keys, meaning most missing records were never sent. Filters are now sized to the table (about 1.8 bytes per key, ~0.1% false positives).
tests/test_mesh_e2e.sh(two real servers: identity, handshake, encrypted request) now passes 14/14, and runs in CI with the other mesh suites.
New docs: compatibility.md (rewritten), images.md, sync.md, mobile.md, app-mode.md. The phar now bundles docs/ so /Q/docs works from it.
v1.5.0 – Boldly Go
Qbix Server v1.5.0 – Boldly Go
What's New Since v1.3
ECDSA P-256 Signing with M-of-N Verification
The Trust system now uses ECDSA P-256 by default (same curve as Sigstore and SSH). Keys are 256 bits instead of RSA's 2048, signatures are 64 bytes instead of 256. Old RSA keys still work — the signer auto-detects the key type.
Multiple signers can each sign the binary independently. At verification time, configure a threshold: "require 2 of 3 signatures to be valid."
# Generate ECDSA keys
./qbixserver --generate-key=alice
./qbixserver --generate-key=bob
# Each signer signs independently
./qbixserver --sign-binary --key=local/keys/alice.pem --signer=Alice
./qbixserver --sign-binary --key=local/keys/bob.pem --signer=Bob
# Verify: need 2 of 2
./qbixserver --verify-binary --m=2Sigstore Rekor Transparency Log
After signing, optionally publish the attestation to Sigstore's public transparency log. Rekor provides an independent, tamper-evident record that the binary was signed at a specific time. A compromised server can't fake this — the Rekor entry is append-only and publicly auditable.
./qbixserver --publish-rekor
# → Published! Rekor UUID: ...
# → Verify: https://search.sigstore.dev/?uuid=...The /Q/attestation endpoint returns both the server's own signatures and the Rekor log entry. Browsers get two independent attestations: the server says "my hash is X, signed by Alice and Bob," and Rekor says "hash X was registered at time T." If they agree, the deployment is what the signers approved.
/Q/attestation Endpoint
Serves the binary hash, signer metadata, verification result, and Rekor reference as JSON. Monitoring tools, browser extensions, or client-side JS can verify the deployment without trusting the server alone.
Security Tab in Control Panel
The panel now has 13 tabs. The new Security tab shows:
- Binary hash (SHA-256) and all signatures with signer names, key IDs, algorithms, dates
- Sign from the browser: paste a PEM private key, name the signer, click Sign
- Verify with adjustable M-of-N threshold
- Publish to Sigstore Rekor with one click (with confirmation — it's permanent and public)
- Rekor log entry link after publishing
- Code Trust status showing per-directory manifest verification results
Metrics and Analytics
Server-side operational metrics with buffered I/O:
Buffered logging — access log lines accumulate in memory (up to 500 lines). Flushed to disk every 10 seconds. Error log has a separate buffer. No disk write per request.
Time-series in SQLite — one row per minute: request count, p50/p95/p99 latency, status code distribution, worker count, memory. Queryable from the dashboard. Retained 30 days by default.
Clickstream analytics — tracks page transitions per session. Each (from_page, to_page) edge accumulates a count. The flow graph powers userflow diagrams on the dashboard. Sessions detected via framework-aware cookie matching (15 frameworks supported) with IP+UA fallback.
Per-page stats — hits, unique sessions, average response time, last hit. Queryable via metrics/pages.
Prometheus endpoint — GET /Q/metrics returns standard gauges and counters in Prometheus text format. Grafana, Datadog, or any scraper can consume it directly.
Log rotation — daily rotation, old logs compressed to .zip, purged after N days (default 7). Zip compression saves 85-90% on repetitive log data.
Anomaly webhook — set Q.webserver.metrics.anomalyWebhook to a URL and the server POSTs a JSON alert on traffic spikes (3× average), error spikes (>10% 5xx), or latency spikes (avg >5s).
Framework-aware session cookies — detects 15 frameworks and knows their session cookie names:
| Framework | Cookie |
|---|---|
| Qbix | Q_session_* (prefix) |
| Laravel | laravel_session |
| WordPress | wordpress_logged_in_* (prefix) |
| Drupal | SESS* (prefix) |
| Symfony | PHPSESSID |
| Magento | frontend, adminhtml |
| Craft CMS | CraftSessionId |
| Moodle | MoodleSession |
| + 7 more | auto-detected |
Autohost — Auto-Provision Domains
Enable Q.webserver.autohost.enabled: true. When a request arrives with an unknown Host header, the server validates the hostname, checks DNS (multi-resolver: system + 1.1.1.1 + 8.8.8.8), provisions a Let's Encrypt cert, writes the domain config, and serves the app. Authorization modes: open, allowlist with glob patterns, or a custom PHP hook.
Watchdog — Auto-Restart on Crash
Fork an independent process that monitors the server and restarts it on crash with exponential backoff. Gives up after 10 crashes in one hour. On clean exit, the watchdog exits too — the shutdown handler explicitly kills it.
Graceful Worker Recycling
Workers track their request count. After maxRequests (default 1000), a worker finishes its current request and is replaced with a fresh fork. Panel controls: recycle a single worker, or "Recycle All" for a rolling restart. Per-worker table shows PID, status, and request count.
Config File Watcher
The server polls config files every 3 seconds. When a file changes, the config is re-read and merged. New requests see new values immediately. No restart needed.
Data Directory for Packed Binaries
When running as a packed binary, data goes to <binary>.data/ instead of ./local/. Subdirectories for logs, certs, and keys created automatically. All components use qbix_data_path() so paths resolve correctly in both packed and normal mode.
SQLite Auto-Provisioning
If your app bundles a .sqlite file at a conventional location, the server copies it to the data directory on first run and writes the framework config to point at it. The seed stays in the zip for factory reset — delete myapp.data/ and re-run to start fresh.
Seed locations checked: database/database.sqlite (Laravel), var/data.db (Symfony), local/db.sqlite (Qbix), data/db.sqlite, or any single .sqlite file in the app root.
Config writing for six frameworks:
- Qbix — modifies
local/app.json, replacesDb.connections.*with SQLite DSN, preserves existing plugin connections, auto-detects installed plugins fromplugins/*/config/plugin.jsonand adds connections with correct table prefixes. Preserves tab indentation and empty objects. - Laravel — sets
DB_CONNECTION=sqliteandDB_DATABASE=/pathin.env. - Symfony — sets
DATABASE_URL=sqlite:///pathin.env. - WordPress — writes
DB_DIRandDB_FILEconstants inwp-config.php(requires wp-sqlite-db drop-in). - Craft CMS — sets
CRAFT_DB_DRIVER=sqlitein.env. - Drupal — appends SQLite driver config to
sites/default/settings.php.
Sets QBIX_DB_PATH environment variable so apps can find the provisioned database.
Hosts File Management
The Domains tab reads /etc/hosts (Windows: drivers\etc\hosts), cross-references configured domains, and offers to add missing entries with platform-specific elevation commands (macOS auth dialog, Windows UAC, Linux pkexec).
Migration Guides
Built-in Cert Renewal
A _certRenewal task runs every 12 hours via the internal scheduler. Scans all certs and renews any expiring within 30 days.
Download
| Platform | Binary | Fork Model |
|---|---|---|
| Linux x86_64 | qbixserver-linux-x86_64 |
pcntl_fork (COW) |
| Linux ARM64 | qbixserver-linux-aarch64 |
pcntl_fork (COW) |
| macOS ARM64 | qbixserver-macos-arm64 |
pcntl_fork (COW) |
| Windows x64 | qbixserver-windows-x64.exe |
RtlCloneUserProcess (COW) |
| Windows x64 | qbixserver-windows-x64-gui.exe |
Same, no console window |
Stats
- 70 unit tests + 30 end-to-end API tests + 25 SQLite provisioning tests = 125 checks, 0 failures
- 13-tab control panel (4,411 lines)
- Trust system with ECDSA + M-of-N + Rekor (780 lines)
- Metrics with clickstream + Prometheus (724 lines)
- SQLite auto-provisioning for 6 frameworks (387 lines)
- 22 documentation files
- ~16,000 lines of PHP + 165 lines of C
v1.4.0 – Boldly Go
Qbix Server v1.4.0 – Boldly Go
Cross-Platform, Single-Binary PHP Apps
This release adds Windows COW fork support, auto-provisioning domains, a watchdog process, single-binary app distribution, and a 12-tab control panel that manages frameworks, packages, workers, domains, certs, and hosts — all from a browser.
Download
| Platform | Binary | Fork Model |
|---|---|---|
| Linux x86_64 | qbixserver-linux-x86_64 |
pcntl_fork (COW) |
| Linux ARM64 | qbixserver-linux-aarch64 |
pcntl_fork (COW) |
| macOS ARM64 | qbixserver-macos-arm64 |
pcntl_fork (COW) |
| Windows x64 | qbixserver-windows-x64.exe |
RtlCloneUserProcess (COW) |
| Windows x64 | qbixserver-windows-x64-gui.exe |
Same, no console window |
Place qbix_fork.dll next to the Windows binary for COW fork support. Without it, the server falls back to php-cgi subprocess mode.
What's New
Windows COW Fork
A 165-line C shim (fork_shim.c) calls the Windows kernel's RtlCloneUserProcess from ntdll.dll via PHP FFI. Each forked worker shares the parent's memory as copy-on-write — the same model that makes it fast on Linux. The Q_WebServer_Fork class (147 lines) abstracts this: pcntl_fork() on Unix, FFI+DLL on Windows, automatic fallback if neither is available.
Single-Binary App Distribution
Pack your entire application into the server binary:
./qbixserver --pack=./my-app --output=myapp
./myapp --openThe binary detects the appended zip at startup by scanning for the ZIP End-of-Central-Directory signature. Standard zip tools can list and extract the contents (unzip -l myapp, 7-Zip on Windows). The --open flag opens the system browser when the server is ready. Set Q.webserver.open: "/" in config for auto-open without flags.
The GUI variant (-gui.exe) has its PE subsystem set to WINDOWS via editbin, so double-clicking it opens the app in a browser with no console window.
Autohost — Auto-Provision Domains on First Request
Enable Q.webserver.autohost.enabled: true and point any domain's DNS at your server. On first request, the server validates the hostname, checks DNS (multi-resolver: system + 1.1.1.1 + 8.8.8.8), provisions a Let's Encrypt cert via the built-in ACME client, writes the domain config, and serves your app. Subsequent requests go straight through.
Authorization modes: open (any hostname), allowlist (glob patterns like *.example.com), or a custom PHP hook file. Rate limiting: per-IP and global caps to stay under Let's Encrypt quotas. DNS mismatches cached 5 minutes, cert failures cached 1 hour.
Serves a branded splash page over plain HTTP while provisioning. No HSTS until a valid cert is in place, so the first-visit flow never triggers a browser warning.
Watchdog — Auto-Restart on Crash
The server forks an independent watchdog process at startup (Q.webserver.watchdog: true). It detaches via setsid, monitors the server, and on crash: logs the exit code and signal, waits with exponential backoff (2s → 4s → ... → 60s), and restarts. Gives up after 10 crashes in one hour. On clean exit (SIGTERM, Ctrl+C), the watchdog exits too — the shutdown handler explicitly kills it.
Graceful Worker Recycling
Workers now track their request count. After maxRequests (default 1000), a worker is replaced with a fresh fork. The recycling is graceful: busy workers finish their current request before being replaced. No in-flight requests are dropped.
New panel controls: recycle a single worker by index, or "Recycle All" for a rolling restart of the entire pool. The Workers tab shows a per-worker table with PID, status (idle/busy/recycling), and request count.
Config File Watcher
The server polls local/app.json, local/panel.json, and config/app.json every 3 seconds. When a file changes, the config is re-read and merged. New requests see new values immediately. No restart, no reload signal, no downtime.
Hosts File Management
The Domains tab reads /etc/hosts (or C:\Windows\System32\drivers\etc\hosts on Windows) and cross-references your configured domains. Domains not in the hosts file get an "Add to hosts" button that provides the platform-specific elevation command (macOS osascript dialog, Windows UAC, Linux pkexec). Domains ending in .localhost are marked as resolving natively — no hosts entry needed.
Control Panel — 12 Tabs
| Tab | What it does |
|---|---|
| Apps | List, create, serve apps. Per-app fork mode toggle. |
| Domains | Manage domains, view cert status, one-click ACME provisioning, hosts file management. |
| Autohost | Toggle auto-provisioning, set authorization mode and allowlist, view provisioning log. |
| Workers | Live pool stats, per-worker table with PID/status/request count, recycle controls. |
| Logs | Real-time access and error log viewer. |
| Cron | Scheduled tasks with "Run Now". Built-in cert renewal (every 12 hours). |
| Frameworks | Auto-detects Laravel, Symfony, WordPress, Drupal, Joomla. Install, update, remove packages. Clone from GitHub. |
| Plugins | Qbix plugin management with three-layer version tracking. npm/composer per plugin. |
| Scripts | Run Qbix installer and other scripts from the panel. |
| Playground | Execute PHP snippets in the server context. |
| System | PHP version, extensions, disk space, phpinfo() iframe. |
| Docs | Browse all 21 markdown docs. |
Docs Viewer
Browse all server documentation at /Q/docs. Dark theme, sidebar nav, marked.js bundled locally (39KB, no CDN). Inline fallback parser if marked.js is missing.
Migration Guides
New documentation for switching from another server:
- Migrating from nginx — server blocks, try_files, proxy_pass, SSL, virtual hosts
- Migrating from Apache — .htaccess works unchanged, VirtualHost mapping
- Migrating from Caddy — automatic HTTPS, on-demand TLS → autohost
GitHub Actions — 4 Platform Build
Builds static binaries for Linux x86_64, Linux ARM64, macOS ARM64, and Windows x64. Windows uses spc from source (not the nightly binary) for reliability. Each platform has separate steps with proper shell handling. fail-fast: false ensures one platform's failure never blocks the others. The Windows build compiles qbix_fork.dll with MSVC and produces a GUI variant via editbin.
Built-in Cert Renewal
A _certRenewal task runs every 12 hours via the internal scheduler. Scans all certs and renews any expiring within 30 days. No external cron job or systemd timer needed.
Framework-Aware App Roots
When autohost provisions a new domain or --pack bundles an app, the server auto-detects the framework and sets the correct web root: web/ for Qbix, public/ for Laravel and Symfony, root for WordPress and Joomla.
Two Modes
Persistent workers (default) — workers stay alive across requests. 28 PHP functions shimmed via source transformation. Static properties restored in 0.03ms. Best performance.
Fork-per-request — for code with untrackable global state. Each worker costs ~120KB (COW), so you run 100× more workers than php-fpm on the same hardware.
Stats
- 70 unit tests + 27 end-to-end API tests, 0 failures
- 21 documentation files
- Panel: 4,222 lines
- Server: 5,618 lines
- Total: ~15,000 lines of PHP + 165 lines of C + 208 lines of watchdog
v1.3.0 – Boldly Go
Full Changelog: v1.2.0...v1.3.0
Qbix Server v1.3.0 – Boldly Go
Cross-Platform Release
This release adds Windows support, a built-in ACME client, a control panel with framework management, and the ability to bundle your app into a single distributable binary.
Windows COW Fork
Qbix Server now runs on Windows with copy-on-write process forking — the same memory model that makes it fast on Linux and macOS. A small DLL (qbix_fork.dll, 15KB) calls the Windows kernel's RtlCloneUserProcess via FFI to fork the server process with COW page sharing. Each worker shares the parent's loaded PHP classes and only allocates memory for pages it writes to during the request.
If the DLL isn't present or the FFI extension isn't available, the server falls back to php-cgi subprocess mode — still functional, just without COW memory savings.
Download
| Platform | Binary | COW Fork |
|---|---|---|
| Linux x86_64 | qbixserver-linux-x86_64 | Yes (pcntl) |
| Linux ARM64 | qbixserver-linux-aarch64 | Yes (pcntl) |
| macOS ARM64 | qbixserver-macos-arm64 | Yes (pcntl) |
| Windows x64 | qbixserver-windows-x64.exe + qbix_fork.dll | Yes (RtlCloneUserProcess) |
What's New
ACME v2 Client — built-in Let's Encrypt integration. Set "tls": "auto" on a domain and the server provisions certificates on startup via HTTP-01 challenges. No certbot needed.
Control Panel — 11 tabs managing every aspect of the server:
- Apps — list, create, serve apps. Per-app fork mode toggle (persistent / fork-per-request / auto).
- Domains — add domains, view cert status, one-click ACME provisioning.
- Workers — live pool stats (count, active, memory, PID).
- Logs — real-time access and error log viewer.
- Cron — scheduled tasks with "Run Now".
- Frameworks — auto-detects Laravel, Symfony, WordPress, Drupal, Joomla. Shows packages from composer.lock, wp-cli, drush, or filesystem scanning. Install, update, remove, activate/deactivate from the panel. Clone plugins from GitHub URLs.
- Plugins (Qbix) — three-layer version tracking: declared (config/app.json), installed (local/plugins.json), schema (Q_plugin table + extra JSON). npm/composer badges per plugin. Run the Qbix installer (
install.php --all) from the panel. - System — PHP version, extensions, disk space, phpinfo() iframe. Key extensions highlighted.
Docs Viewer — browse all 18 markdown docs at /Q/docs. Dark theme, sidebar nav, marked.js bundled locally (no CDN). Links between docs work. Offline fallback parser if marked.js is missing.
--pack Flag — bundle your app into the binary: ./qbixserver --pack=./my-app -o myapp. The binary detects the appended zip at startup and serves from it. Standard zip tools can list and extract the contents. Distribute a single executable file containing the PHP runtime, the server, and your entire application.
System Limits Check — on startup, reads ulimit -n, ulimit -u, pid_max. Tries to raise limits automatically. Warns with platform-specific fix commands if it can't.
Dashboard Enhancements — system RAM usage (Linux/macOS/Windows), per-worker RSS from /proc/ps/tasklist, COW savings comparison, fork mode indicator.
Framework Presets
php qbixserver.php --root=public --preset=laravel
php qbixserver.php --root=public --preset=symfony
php qbixserver.php --root=. --preset=wordpress
php qbixserver.php --root=web --preset=drupal
Two Modes
Persistent workers (default) — workers stay alive across requests. 28 PHP functions shimmed via source transformation. Static properties restored in 0.03ms. Best performance: 2,294 req/s on CPU-bound work.
Fork-per-request — set forkPerRequest: true for code with internal static variables or untrackable global state. Each worker costs ~120KB (COW), so you run 100× more workers than php-fpm on the same hardware.
Bug Fixes
- Pool superglobals (SERVER_SOFTWARE, PHP_SELF, etc.) not set in persistent workers
- Multipart parsing passed lowercased content-type
- Health endpoint 500 after docs route insertion
- Safe worker calculation returned PHP_INT_MAX when fd limit was low
//comment fallback missing in API doc generation
Stats
- 72 unit tests + 55 end-to-end tests, 0 failures
- README split into 18 doc files with relative links
- Panel: 3,882 lines
- WebServer: 5,554 lines
- Total server code: ~13,000 lines of PHP + 165 lines of C
v1.2.0 - Boldly Go
Persistent workers are now the default mode. No flags needed — the server auto-detects worker count from your RAM and CPU. Unmodified WordPress, Laravel, Symfony, and Drupal run 6–24× faster than php-fpm on the same hardware, without code changes.
What's new
Persistent workers as default
Workers auto-detect: min(available_RAM / 200KB, nproc × 200). On a 4GB machine that's 400 workers at 120KB each = 47MB total. No --workers flag needed.
28 shimmed functions (was 19)
Nine new shims: register_shutdown_function, set_error_handler, set_exception_handler, restore_error_handler, restore_exception_handler, spl_autoload_register, spl_autoload_unregister, putenv, plus enhanced ini_set tracking with per-request restore. All tracked per request and cleaned up between requests.
Cached Reflection snapshot restore
ReflectionProperty objects are cached at snapshot time. Restore does only setValue() calls — no per-request Reflection lookups. 31% faster on hello-world, 46% faster on WordPress-like workloads.
Benchmarks (single CPU, PHP 8.3)
| Workload | php-fpm | Swoole | Qbix | vs fpm |
|---|---|---|---|---|
| CPU-bound (WP-like) | ~350 req/s | ~400 req/s | 2,294 req/s | 6.6× |
| I/O 50ms (c=200) | 78 req/s | ~300 req/s* | 1,060 req/s | 14× |
| I/O 200ms (c=400) | 20 req/s | ~200 req/s* | 488 req/s | 24× |
*Swoole coroutines require Runtime::enableCoroutine() and coroutine-aware drivers. Unmodified PHP code uses blocking I/O and hits fpm's ceiling.
API discovery — three formats from one scan
Every app's handlers/ directory is scanned automatically:
/.well-known/openapi.json— OpenAPI 3.1 (Swagger UI, Postman, Redoc)/.well-known/mcp.json— MCP tools for AI assistants (Claude, GPT, Cursor)/.well-known/qbix.json— Federation manifest
Supports PHPDoc, YUIDoc, and bare // comment formats. No annotations required.
Pool worker fixes
$_SERVER:SERVER_SOFTWARE,GATEWAY_INTERFACE,SERVER_PROTOCOL,REQUEST_SCHEME,PHP_SELFPHP_AUTH_USER/PHP_AUTH_PWparsed from Basic auth- HTTPS detection from
X-Forwarded-Proto - Multipart form parsing (
$_POST+$_FILES) - Status codes from
Q_WebServer_State::getStatusCode() php://inputstream wrapper for persistent workers//comment fallback for API doc generation
Config
{
"forkPerRequest": false,
"skipSourceCodeTransform": false
}Both default to false — persistent workers with full shimming. Falsy defaults, descriptive names.
Test results
72/72 passing (was 63/72 in v1.1.0).
Full Changelog: v1.1.0...v1.2.0
v1.1.0 — Boldly Go
Full Changelog: v1.0.0...v1.1.0
v1.0.0 — Boldly Go
A pure PHP web server that replaces nginx + php-fpm. One process serves static files, PHP scripts, WebSocket connections, and a live dashboard.
Install
Download a binary — no PHP installation needed:
| Platform | Download |
|---|---|
| Linux x86_64 | qbixserver-linux-x86_64 |
| Linux ARM64 | qbixserver-linux-aarch64 |
| macOS ARM64 | qbixserver-macos-arm64 |
chmod +x qbixserver-linux-x86_64
./qbixserver-linux-x86_64Or if you already have PHP 8.1+: php qbixserver.php
What's in the binary
PHP 8.3 interpreter (statically linked), SQLite3, OpenSSL, curl, mbstring, pcntl, sockets, posix, phar, session.
Highlights
- Two execution modes: fork-per-request (shared-nothing) and octane (
--workers=Nwith snapshot restore) - Over 100× the concurrent capacity of nginx + php-fpm on the same memory
- WebSocket with Socket.IO v5 protocol and rooms
- SSE / streaming with auto-detection
- Unix domain socket listen (
--socket=/path) for nginx proxy - X-Accel-Redirect access control and X-Cache-Tree component-level caching
- Live dashboard at
/Q/dashboard - Full Qbix Platform compatibility — same APIs, easy migration to
--appmode - 160+ tests across 6 suites