# ⚑ Qbix Server A pure PHP web server. No nginx, no Apache, no php-fpm. One process serves static files, PHP scripts, WebSocket connections, and a live dashboard. **55–73% of nginx throughput** on static files. Zero dependencies beyond PHP itself. --- ## πŸ“‘ Table of Contents - [Quick Start](#-quick-start) - [Performance](#-performance) - [Why Not php-fpm?](#-why-not-php-fpm) - [Features](#-features) - [Server Powers](#-server-powers--what-your-php-can-do) - [Configuration](#-configuration) - [PHP Scripts](#-php-scripts) - [Three Ways to Run](#-three-ways-to-run) - [Building](#-building) - [With Qbix Platform](#-with-qbix-platform) - [Architecture](#-architecture) - [Requirements](#-requirements) - [License](#-license) --- ## πŸš€ Quick Start ```bash # Clone git clone https://github.com/Qbix/Server.git cd Server # Create a web directory mkdir web echo '

Hello World

' > web/index.html # Run php server.php --port=8080 ``` Open [http://localhost:8080](http://localhost:8080). That's it. ```bash # Or serve an existing directory php server.php --root=/var/www/mysite --port=80 # Or use the PHAR (single file, 196KB) php bin/qbix-server.phar --root=./public --port=8080 ``` --- ## πŸ“Š Performance Benchmarked against nginx on the same single-core container, PHP 8.3, Ubuntu 24. 13KB static file, best-of-3 runs, warm caches. | Scenario | nginx | Qbix Server | Ratio | |---|---|---|---| | Sequential (c=1) | 10,154 req/s | 6,376 req/s | **63%** | | Concurrent (c=10) | 12,300 req/s | 6,876 req/s | **56%** | | High concurrency (c=50) | 12,919 req/s | 7,253 req/s | **56%** | | Keep-alive (c=10) | 26,858 req/s | 19,700 req/s | **73%** | | Keep-alive (c=50) | 30,158 req/s | 20,369 req/s | **67%** | Zero failed requests across 50,000+ requests at concurrency 50. Server never crashed. > For context: 20K req/s means the server handles **1,000 simultaneous page loads per second** > (assuming ~20 static assets per page), all from a single PHP process. --- ## 🏎️ Why Not php-fpm? The traditional stack β€” nginx + php-fpm β€” works like this: ``` Request β†’ nginx β†’ FastCGI socket β†’ php-fpm worker ↓ Load PHP Include autoloader Boot framework Connect to DB Run your code Send response ↓ Worker resets or dies ``` Every PHP request pays the bootstrap cost. Even with OPcache, each php-fpm worker re-initializes your framework's class instances, config trees, and DB connections on every request. For a framework like Qbix (or Laravel, Symfony, etc.), this bootstrap takes **10–50ms** β€” often longer than the actual work. **Qbix Server eliminates this entirely:** ``` Startup: 1. Load PHP 2. Include autoloader 3. Load ALL framework classes into memory 4. Parse ALL config files 5. Connect to database 6. pcntl_fork() β†’ workers inherit everything ↓ Request: Worker already has classes, config, DB connections. Just run your code. 0ms bootstrap. ``` The key insight is **fork after preload**. Unix `fork()` uses copy-on-write, so forked workers share the parent's memory pages for all those preloaded classes. Each worker starts with ~30MB shared (read-only) and allocates only the per-request data. Compare this to php-fpm where each worker loads everything independently, using 30MB Γ— N workers of duplicated memory. ### Preloading classes Use the `--workers=N` flag and configure which classes to preload: ```json { "Q": { "webserver": { "preload": [ "Q_Dispatcher", "Q_Request", "Q_Response", "Q_Config", "Q_Cache", "Q_Session", "Db", "Db_Mysql", "Db_Row", "Db_Query", "Users", "Users_User", "Users_Session", "Streams", "Streams_Stream", "Streams_Message" ] } } } ``` ```bash # Start with 4 workers (classes loaded once, shared across all) php server.php --app=/path/to/myapp --port=8080 --workers=4 ``` The parent process loads and parses every class in the `preload` list, then forks. Workers inherit the entire loaded state β€” OPcache entries, class definitions, parsed config trees, autoloader maps. The first PHP request in each worker runs at full speed, no cold start. ### The numbers | | php-fpm | Qbix Server | |---|---|---| | Bootstrap per request | 10–50ms | **0ms** | | Memory per worker | 30–60MB each | 30MB shared + ~5MB per worker | | IPC overhead | FastCGI socket + serialization | Direct function call or Unix fork | | Static files | Separate nginx process | Same process, memory-cached, single `fwrite` | | Config reload | Restart all workers | `SIGHUP`, zero downtime | | WebSocket | Needs separate server | Built in | For a Qbix app with 20 loaded plugins, the bootstrap savings alone make the server **2–5x faster** on PHP requests compared to nginx + php-fpm. ### Why it's actually faster in practice The benchmarks above measure static file throughput, where nginx's C implementation and `sendfile()` syscall give it an inherent edge. But for **real PHP applications**, the story flips: - **nginx + php-fpm:** 0.1ms static file + 30ms PHP bootstrap + 5ms actual work = **35ms** - **Qbix Server:** 0.15ms static file + 0ms bootstrap + 5ms actual work = **5ms** The 0.05ms you lose on static files, you gain back 30ms on every PHP request. And you can always put nginx or a CDN in front for the static file edge. --- ## ✨ Features | Category | What you get | |---|---| | **Static files** | ETag, 304 Not Modified, Last-Modified, MIME type detection, in-memory response cache | | **Keep-alive** | HTTP/1.0 and 1.1, TCP_NODELAY, configurable limits | | **PHP execution** | `.php` files in document root run in-process or via pre-fork worker pool | | **Compression** | On-the-fly gzip/brotli + pre-compressed `.gz`/`.br` siblings | | **WebSocket** | RFC 6455 upgrade on any path | | **Dashboard** | Live stats at `/Q/dashboard` β€” request rates, memory, status codes | | **Health check** | JSON at `/Q/health` β€” for load balancers and monitoring | | **Control panel** | Password-protected at `/Q/panel` β€” manage apps and scripts | | **Rate limiting** | Per-IP with configurable windows and burst limits | | **Security** | Path traversal blocked, dotfiles blocked, 431 for oversized headers, 400 for malformed requests | | **Graceful shutdown** | SIGTERM/SIGINT drain in-flight requests before closing | | **TLS** | Optional HTTPS with auto-certbot or manual certs | | **Logging** | Colored terminal output + file-based access logs | | **Access control** | X-Accel-Redirect support β€” PHP enforces access, server serves the file | | **Component cache** | X-Cache-Tree headers β€” invalidate parts of a page, not the whole thing | --- ## πŸ”’ Server Powers β€” What Your PHP Can Do Qbix Server understands special response headers from your PHP scripts, giving you capabilities that normally require complex nginx configurations or aren't possible at all. ### Access-controlled static files With a typical server, your uploaded files sit at public URLs. Anyone with the link can access them β€” and share the link with others. The usual workaround is "unguessable" URLs, which are just security through obscurity. Qbix Server supports `X-Accel-Redirect`: your PHP checks access, then tells the server to serve the file directly β€” fast, streamed, with no public URL exposed: ```php [ 'feed' => $feedHash, 'sidebar' => $sidebarHash, 'members' => $membersHash, ] ])); header('X-Cache-Deps: ' . json_encode([ 'feed' => ["community/{$communityId}/feed"], 'sidebar' => ["community/{$communityId}/about"], 'members' => ["community/{$communityId}/participants"], ])); // When someone posts to the feed, only 'feed' is invalidated. // The sidebar and members list are still served from cache. // The server re-renders only the stale component. ``` When data changes, tell the server which dependency key was affected: ```php true, 'maxAge' => 300, 'mustRevalidate' => true, ]); ``` The Platform's Streams plugin automatically invalidates cache dependencies when stream data changes β€” posts, relations, participant joins β€” so cached pages update themselves without manual invalidation calls. Combined with the server's Merkle tree, this gives you fine-grained, data-driven cache invalidation across your entire app, with zero configuration. --- ## βš™οΈ Configuration Create `config/server.json` next to your `web/` directory, or pass `--config=path/to/config.json`: ```json { "Q": { "webserver": { "keepAlive": { "max": 100, "timeout": 15 }, "maxConnections": 1024, "fileCache": { "maxSize": 67108864, "maxFile": 1048576, "checkInterval": 1 }, "rateLimit": { "enabled": true, "requests": 100, "window": 60 } } } } ``` | Key | Default | What it does | |---|---|---| | `keepAlive.max` | 100 | Max requests per keep-alive connection | | `keepAlive.timeout` | 15 | Seconds before closing idle connection | | `maxConnections` | 1024 | Max simultaneous connections | | `fileCache.maxSize` | 64MB | Total memory for cached file responses | | `fileCache.maxFile` | 1MB | Largest file to cache in memory | | `fileCache.checkInterval` | 1 | Seconds between file modification checks | | `rateLimit.enabled` | false | Enable per-IP rate limiting | | `rateLimit.requests` | 100 | Requests per window | | `rateLimit.window` | 60 | Window in seconds | --- ## 🐘 PHP Scripts Any `.php` file in your document root is executed when requested: ``` web/ index.html ← served as static file style.css ← served as static file api.php ← executed as PHP webhook.php ← executed as PHP ``` PHP scripts have full access to `$_SERVER`, `$_GET`, `$_POST`, `$_REQUEST`: ```php date('c'), 'method' => $_SERVER['REQUEST_METHOD'], 'query' => $_GET, ]); ``` For concurrent PHP execution, use `--workers=N` to pre-fork a worker pool. --- ## πŸ“¦ Three Ways to Run ### 1. From source (needs PHP 8.1+) ```bash php server.php --root=./web --port=8080 ``` ### 2. PHAR β€” single 196KB file (needs PHP) ```bash php bin/qbix-server.phar --root=./web --port=8080 # Or make it executable chmod +x bin/qbix-server.phar ./bin/qbix-server.phar --port=8080 ``` ### 3. Static binary β€” no PHP needed ```bash # Download from GitHub Releases chmod +x qbix-server-linux-x86_64 ./qbix-server-linux-x86_64 --root=./web --port=8080 ``` The binary bundles PHP 8.3 + extensions into a single ~15MB executable. Copy it to any Linux or macOS machine and run. No dependencies. --- ## πŸ”¨ Building ### Build the PHAR ```bash php -d phar.readonly=0 build-phar.php # Output: bin/qbix-server.phar ``` ### Build the static binary ```bash # With Docker (easiest): ./build-binary.sh --docker # With static-php-cli installed locally: ./build-binary.sh # Output: bin/qbix-server (~15MB) ``` The binary is built using [static-php-cli](https://github.com/crazywhalecc/static-php-cli), which compiles PHP + extensions into a statically linked binary. GitHub Actions automatically builds binaries for **Linux x86_64**, **Linux ARM64**, **macOS x86_64**, and **macOS Apple Silicon** on every tagged release. --- ## πŸ”Œ With Qbix Platform Qbix Server is extracted from the [Qbix Platform](https://github.com/Qbix/Platform) β€” a full-stack framework for building social apps with real-time streams, user management, and plugin architecture. When you have a Qbix app, the server uses the full framework: ```bash php server.php --app=/path/to/myapp --port=8080 ``` In this mode: - Requests route through `Q_Dispatcher` β€” the full Qbix event pipeline - Plugins load automatically (Users, Streams, Assets, etc.) - Clean URLs work (`/community/123` β†’ module routing) - Static files still use the fast path (no framework overhead) - The dashboard shows Qbix-specific stats The standalone mode (without `--app`) runs as a plain web server β€” no framework, no plugins. PHP files execute directly, static files serve from memory. Use this for simple sites, APIs, or any project that doesn't need the full Qbix stack. ### Qbix Platform scripts The full Platform includes additional server scripts like `static.php` for CDN-style static file serving with versioned URLs. See the [Platform repository](https://github.com/Qbix/Platform) for details. --- ## πŸ—οΈ Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” HTTP request ────→ β”‚ Event Loop β”‚ stream_select (zero deps) β”‚ (single thread) β”‚ or amphp/revolt (optional) β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β” β”‚ Static β”‚ β”‚ PHP β”‚ β”‚ WebSocket β”‚ β”‚ Files β”‚ β”‚ Dispatch β”‚ β”‚ Upgrade β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ In-memoryβ”‚ β”‚ In-proc β”‚ β”‚ RFC 6455 β”‚ β”‚ response β”‚ β”‚ or fork β”‚ β”‚ frames β”‚ β”‚ cache β”‚ β”‚ pool β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **Static files** are served from an in-memory response cache. The full HTTP response (headers + body) is pre-built and sent in a single `fwrite()` call. The cache is mtime-validated with configurable check intervals. Combined with `TCP_NODELAY`, this delivers sub-millisecond response times. **PHP scripts** run in-process (single-threaded, suitable for lightweight APIs) or in a pre-fork worker pool (`--workers=N`) for concurrent PHP execution. Workers are forked after class preloading, so they share the base memory footprint via copy-on-write pages. **The remaining gap** versus nginx (55–73%) is inherent: nginx uses `sendfile()` (kernel-space fileβ†’socket copy), `epoll` (O(1) event notification), and compiled C. PHP's `stream_select` is `select(2)`, file serving goes through userspace, and every operation has interpreter overhead. Getting to 55–73% of C performance from pure interpreted PHP is about as good as it gets. --- ## πŸ“‹ Requirements **For server.php and PHAR:** - PHP 8.1 or later - Extensions: `sockets`, `pcntl` (for signals + workers), `openssl` (for HTTPS) ```bash # Check php -m | grep -E 'sockets|pcntl|openssl' # Install on Ubuntu/Debian sudo apt install php-cli php-sockets ``` **For the static binary:** - Nothing. The PHP runtime is included. --- ## πŸ“„ License MIT β€” see [LICENSE](LICENSE). Part of the [Qbix Platform](https://github.com/Qbix/Platform).