2026-07-20 10:26:59 -04:00
# ⚡ 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.
2026-07-20 10:54:19 -04:00
### Why it's faster than nginx + php-fpm for real apps
| | nginx + php-fpm | Qbix Server |
|---|---|---|
| 🚀 **PHP request speed** | 10– 50ms bootstrap on *every* request | **0ms** — workers fork after classes are loaded |
| 💾 **Memory** | 30– 60MB × N workers (duplicated) | 30MB shared + ~5MB per worker (copy-on-write) |
| 🔒 **Access-controlled files** | Public URLs or hacky rewrites | `X-Accel-Redirect` — PHP checks access, server streams the file |
| 🧩 **Cache invalidation** | Whole-page only (purge everything) | `X-Cache-Tree` — invalidate one component, keep the rest cached |
| 🌐 **WebSocket** | Needs a separate server | Built in |
| ⚙️ **Setup** | Install nginx, configure proxy_pass, php-fpm pool, sockets... | `php qbixserver.php --port=8080` |
Static file throughput is 55– 73% of nginx (C will always beat PHP on raw I/O).
But on **actual PHP workloads** , the bootstrap savings make this **2– 5x faster** .
> 💡 You can always put nginx, a reverse proxy, or a CDN (Cloudflare, CloudFront)
> in front of this for faster HTTPS and edge caching. Qbix Server handles the
> PHP execution, access control, and intelligent caching behind it.
2026-07-20 10:26:59 -04:00
---
## 📑 Table of Contents
- [Quick Start ](#-quick-start )
- [Performance ](#-performance )
2026-07-20 10:47:22 -04:00
- [Why Not php-fpm? ](#-why-not-php-fpm )
2026-07-20 10:58:37 -04:00
- [vs FrankenPHP and Swoole ](#️ -vs-frankenphp-and-swoole )
2026-07-20 10:26:59 -04:00
- [Features ](#-features )
2026-07-20 11:50:30 -04:00
- [Server Headers ](#-server-headers--what-your-php-can-send )
- [Project Structure ](#-project-structure )
2026-07-20 10:26:59 -04:00
- [Configuration ](#-configuration )
- [Three Ways to Run ](#-three-ways-to-run )
- [Building ](#-building )
- [With Qbix Platform ](#-with-qbix-platform )
- [Architecture ](#-architecture )
2026-07-20 10:50:05 -04:00
- [HTTP/2 Support ](#-http2-support )
2026-07-20 10:26:59 -04:00
- [Requirements ](#-requirements )
- [License ](#-license )
---
## 🚀 Quick Start
```bash
# Clone
2026-07-20 10:47:22 -04:00
git clone https://github.com/Qbix/Server.git
cd Server
2026-07-20 10:26:59 -04:00
# Create a web directory
mkdir web
echo '<h1>Hello World</h1>' > web/index.html
# Run
2026-07-20 10:54:19 -04:00
php qbixserver.php --port= 8080
2026-07-20 10:26:59 -04:00
```
Open [http://localhost:8080 ](http://localhost:8080 ). That's it.
```bash
# Or serve an existing directory
2026-07-20 10:54:19 -04:00
php qbixserver.php --root= /var/www/mysite --port= 80
2026-07-20 10:26:59 -04:00
# Or use the PHAR (single file, 196KB)
2026-07-20 10:54:19 -04:00
php bin/qbixserver.phar --root= ./public --port= 8080
2026-07-20 10:26:59 -04:00
```
---
## 📊 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.
---
2026-07-20 10:47:22 -04:00
## 🏎️ 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)
2026-07-20 10:54:19 -04:00
php qbixserver.php --app= /path/to/myapp --port= 8080 --workers= 4
2026-07-20 10:47:22 -04:00
```
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.
---
2026-07-20 10:58:37 -04:00
## ⚖️ vs FrankenPHP and Swoole
If you're looking beyond php-fpm, you've probably seen FrankenPHP and Swoole. Here's how they compare:
| | FrankenPHP | Swoole | Qbix Server |
|---|---|---|---|
| **Language** | Go + C (embeds PHP) | C extension for PHP | Pure PHP |
| **Install** | Download Go binary or Docker | `pecl install swoole` (compiles C) | `php qbixserver.php` — nothing to install |
| **Architecture** | Worker mode (persistent) | Coroutine-based (persistent) | Shared-nothing with fork-after-preload |
| **State leaks** | ⚠️ Possible — workers persist between requests | ⚠️ Possible — must manage globals carefully | ✅ Impossible — each request gets a clean fork |
| **PHP compatibility** | Most code works, some edge cases | Many extensions incompatible, blocking I/O breaks coroutines | ✅ 100% — standard PHP, nothing unusual |
| **Memory safety** | Go runtime + PHP = complex interaction | C extension = segfault risk | PHP only = memory-safe by default |
| **Access control** | No X-Accel-Redirect equivalent | Manual implementation | ✅ Built-in X-Accel-Redirect |
| **Component cache** | No | No | ✅ X-Cache-Tree — sub-page invalidation |
| **Early hints / 103** | ✅ Yes | No | Via amphp |
| **HTTP/2** | ✅ Built-in (Caddy) | ✅ Built-in | ✅ Via amphp |
| **WebSocket** | Via Mercure | ✅ Built-in | ✅ Built-in |
### The shared-nothing advantage
FrankenPHP and Swoole keep PHP workers alive across requests. This is fast, but it means global state, static variables, database connections, and in-memory caches **persist between unrelated requests** . This causes subtle bugs:
```php
// This leaks between requests in FrankenPHP/Swoole:
class UserService {
private static ? User $cached = null ;
public static function current () : User {
if ( ! self :: $cached ) {
self :: $cached = User :: fromSession ();
}
return self :: $cached ; // Returns previous user's data!
}
}
```
Every PHP framework, library, and snippet that uses static variables, singletons, or global state becomes a potential security hole. You have to audit everything.
Qbix Server avoids this entirely. Workers fork from a preloaded parent, so they inherit loaded classes and parsed config (read-only, shared via copy-on-write). But each request runs in its own process — when it's done, everything is gone. No state leaks. No audit needed. Your existing PHP code works exactly as it does on php-fpm.
### The "just PHP" advantage
FrankenPHP requires Go tooling to build or a pre-built binary that bundles Caddy. Swoole requires compiling a C extension, which can conflict with other extensions and doesn't work on all hosting environments.
Qbix Server is a PHP file. If you can run `php -v` , you can run the server. It uses standard PHP extensions (`sockets` , `pcntl` ) that come pre-installed on most systems. There's no compilation step, no foreign runtime, no binary compatibility issues.
```bash
# FrankenPHP
docker pull dunglas/frankenphp # 150MB+ image, or build from Go source
# Swoole
pecl install swoole # compiles C, may fail on some systems
# Then edit php.ini, restart php...
# Qbix Server
php qbixserver.php --port= 8080 # done
```
### When to choose what
**Choose FrankenPHP** if you want Caddy's ecosystem (automatic HTTPS, HTTP/3) and don't mind Go as a dependency. Good for Laravel projects that already use Octane.
**Choose Swoole** if you need coroutines for high-concurrency I/O (thousands of simultaneous HTTP client requests, database queries). Good for async-heavy microservices.
**Choose Qbix Server** if you want shared-nothing safety, zero-install deployment, access-controlled file serving, component-level cache invalidation, and full compatibility with existing PHP code. Good for apps that serve pages (not just APIs), need fine-grained caching, and want the simplest possible deployment.
---
2026-07-20 10:26:59 -04:00
## ✨ 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 |
2026-07-20 10:50:05 -04:00
| **HTTP/2** | Via amphp — multiplexed streams, header compression, TLS (optional) |
2026-07-20 10:26:59 -04:00
| **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 |
2026-07-20 10:47:22 -04:00
| **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 |
---
2026-07-20 11:50:30 -04:00
## 🔒 Server Headers — What Your PHP Can Send
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
Qbix Server understands special response headers from your PHP scripts. These are
the same headers nginx understands (like `X-Accel-Redirect` ) plus new ones for
component-level caching. Your PHP sends them with `header()` , the server acts on them.
### Quick reference
| Header | What it does | Example |
|---|---|---|
| `Cache-Control` | Server caches the response, serves without running PHP | `header('Cache-Control: public, max-age=300');` |
| `X-Accel-Redirect` | Server streams a file after PHP checks access | `header('X-Accel-Redirect: /uploads/private/doc.pdf');` |
| `X-Cache-Tree` | Registers page components with content hashes | `header('X-Cache-Tree: ' . json_encode([...]));` |
| `X-Cache-Deps` | Maps components to data dependency keys | `header('X-Cache-Deps: ' . json_encode([...]));` |
| `X-Cache-Invalidate` | Marks dependency keys as stale | `header('X-Cache-Invalidate: ' . json_encode([...]));` |
| `X-Cache-Stale` | Marks specific components as needing re-render | `header('X-Cache-Stale: feed,sidebar');` |
All of these are standard PHP `header()` calls. No SDK, no framework needed.
The server strips them before sending the response to the client.
2026-07-20 10:47:22 -04:00
### 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.
2026-07-20 11:50:30 -04:00
`X-Accel-Redirect` lets your PHP check access, then tells the server to serve the file
directly — fast, streamed, with no public URL exposed:
2026-07-20 10:47:22 -04:00
```php
<? php
// web/download.php — access-controlled file serving
session_start ();
$fileId = $_GET [ 'id' ] ?? '' ;
$userId = $_SESSION [ 'user_id' ] ?? null ;
// Your access control logic
if ( ! $userId || ! userCanAccess ( $userId , $fileId )) {
http_response_code ( 403 );
echo 'Access denied' ;
exit ;
}
// Tell the server to serve the file directly.
// The client never sees the real path.
2026-07-20 11:50:30 -04:00
header ( "X-Accel-Redirect: /uploads/private/ { $fileId } " );
2026-07-20 10:47:22 -04:00
header ( "Content-Disposition: attachment; filename= \" document.pdf \" " );
// The server takes over from here — streams the file
// with correct Content-Type, ETag, compression, etc.
// Your PHP process is already done.
```
No public URL for the file. No redirect the user can bookmark. The server streams
2026-07-20 11:50:30 -04:00
the file after your PHP has verified access and exited.
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
### Reverse proxy cache
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
Control how the server caches your PHP responses:
2026-07-20 10:47:22 -04:00
```php
<? php
// web/feed.php — cached for 5 minutes
2026-07-20 11:50:30 -04:00
// The server caches this response and serves it without
// running PHP again for the next 300 seconds.
2026-07-20 10:47:22 -04:00
header ( 'Cache-Control: public, max-age=300' );
echo renderFeed ();
```
2026-07-20 11:50:30 -04:00
```php
<? php
// web/profile.php — cached, but revalidate with ETag
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
// The server generates an ETag from the response body.
// Browsers send If-None-Match on next request.
// Server returns 304 (no body) if nothing changed.
header ( 'Cache-Control: public, max-age=0, must-revalidate' );
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
echo renderProfile ( $userId );
```
2026-07-20 10:47:22 -04:00
```php
<? php
2026-07-20 11:50:30 -04:00
// web/admin.php — never cache
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
header ( 'Cache-Control: no-store' );
2026-07-20 10:47:22 -04:00
2026-07-20 11:50:30 -04:00
echo renderAdminPanel ();
```
### Component-level cache invalidation
Most caching systems cache whole pages. When anything changes, you throw away the
entire page and re-render everything. Qbix Server can cache individual components
and only re-render what changed.
**Step 1: Register components when rendering a page**
```php
<? php
// web/community.php — a page with three components
$feedHtml = renderFeed ( $communityId );
2026-07-20 10:47:22 -04:00
$sidebarHtml = renderSidebar ( $communityId );
$membersHtml = renderMembers ( $communityId );
2026-07-20 11:50:30 -04:00
// Tell the server about the component tree and what data each depends on
2026-07-20 10:47:22 -04:00
header ( 'X-Cache-Tree: ' . json_encode ([
'l' => [
2026-07-20 11:50:30 -04:00
'feed' => md5 ( $feedHtml ),
'sidebar' => md5 ( $sidebarHtml ),
'members' => md5 ( $membersHtml ),
2026-07-20 10:47:22 -04:00
]
]));
header ( 'X-Cache-Deps: ' . json_encode ([
'feed' => [ "community/ { $communityId } /feed" ],
'sidebar' => [ "community/ { $communityId } /about" ],
'members' => [ "community/ { $communityId } /participants" ],
]));
2026-07-20 11:50:30 -04:00
header ( 'Cache-Control: public, max-age=300' );
echo $feedHtml . $sidebarHtml . $membersHtml ;
2026-07-20 10:47:22 -04:00
```
2026-07-20 11:50:30 -04:00
**Step 2: Invalidate when data changes**
2026-07-20 10:47:22 -04:00
```php
<? php
// web/post.php — user posts to the feed
saveNewPost ( $communityId , $content );
2026-07-20 11:50:30 -04:00
// Tell the server which dependency key changed
2026-07-20 10:47:22 -04:00
header ( 'X-Cache-Invalidate: ' . json_encode ([
"community/ { $communityId } /feed"
]));
// The server walks its dependency graph:
// community/123/feed → page /community/123 component 'feed'
2026-07-20 11:50:30 -04:00
// Only 'feed' is stale. Sidebar, members = still cached.
// Next request re-renders only the feed component.
echo json_encode ([ 'ok' => true ]);
2026-07-20 10:47:22 -04:00
```
2026-07-20 11:50:30 -04:00
The server maintains a Merkle tree of component hashes. When a dependency key is
invalidated, it walks the tree to find exactly which components on which pages are
affected. Everything else is served from the in-memory cache.
2026-07-20 10:47:22 -04:00
### Even more powerful with Qbix Platform
2026-07-20 11:50:30 -04:00
These headers work with plain PHP `header()` calls as shown above. But with the
2026-07-20 10:47:22 -04:00
[Qbix Platform ](https://github.com/Qbix/Platform ), it becomes automatic:
```php
// Tools call this during rendering — the framework handles the rest
Q_Response :: setCacheComponent ( 'Streams/feed' , $hash , [ $depKey ]);
Q_Response :: invalidateCacheDeps ( $publisherId . '/' . $streamName );
// X-Accel-Redirect for access-controlled files
2026-07-20 11:50:30 -04:00
Q_Response :: redirect ([ 'uri' => $internalPath , 'accel' => true ]);
2026-07-20 10:47:22 -04:00
// Cache-Control with semantic options
2026-07-20 11:50:30 -04:00
Q_Response :: cacheFor ( 300 );
2026-07-20 10:47:22 -04:00
```
The Platform's Streams plugin automatically invalidates cache dependencies when
stream data changes — posts, relations, participant joins — so cached pages
2026-07-20 11:50:30 -04:00
update themselves without any manual invalidation calls.
---
## 📂 Project Structure
A typical project looks like this:
```
myproject/
├── qbixserver.php ← server entry point (or use the PHAR)
├── config/
│ └── server.json ← server configuration
├── web/ ← document root (publicly accessible)
│ ├── index.html
│ ├── style.css
│ ├── app.js
│ ├── api.php ← executed as PHP
│ └── uploads/ ← served as static files (or via X-Accel-Redirect)
└── classes/ ← your PHP classes (for preloading)
├── MyApp.php
├── MyApp/
│ ├── User.php
│ ├── Feed.php
│ └── Auth.php
└── vendor/ ← composer dependencies
└── autoload.php
```
Only files under `web/` are accessible via HTTP. The `classes/` directory is for
code that runs inside your PHP scripts — and for classes you want preloaded into
the fork pool.
### Preloading classes
When you use `--workers=N` , the parent process loads classes before forking.
Workers inherit everything via copy-on-write — zero bootstrap cost per request.
Configure preloading in `config/server.json` :
```json
{
"Q" : {
"webserver" : {
"preload" : {
"autoload" : "classes/vendor/autoload.php" ,
"classes" : [
"MyApp" ,
"MyApp\\User" ,
"MyApp\\Feed" ,
"MyApp\\Auth" ,
"MyApp\\Database"
]
}
}
}
}
```
```bash
# Start with 4 workers — classes loaded once, shared across all
php qbixserver.php --root= ./web --port= 8080 --workers= 4
```
What happens at startup:
```
1. Parent includes classes/vendor/autoload.php
2. Parent loads each class in the preload list (triggers autoloader)
3. All class definitions, constants, and autoloader maps are now in memory
4. Parent calls pcntl_fork() × 4
5. Each worker inherits everything — ready to handle requests immediately
```
Your `web/api.php` can now use `MyApp\User` or `MyApp\Feed` without any `require`
or autoloader overhead — the classes are already loaded:
```php
<? php
// web/api.php — classes are already in memory from preloading
use MyApp\User ;
use MyApp\Feed ;
$user = User :: fromSession ();
$items = Feed :: latest ( 20 );
header ( 'Content-Type: application/json' );
header ( 'Cache-Control: public, max-age=60' );
echo json_encode ( $items );
```
### Workers: fork-per-request (truly shared-nothing)
Each worker handles exactly **one request** , then exits. The parent immediately
forks a replacement. This means:
- Static variables — **wiped** (process dies)
- Global state — **wiped** (process dies)
- Memory leaks — **impossible** (OS reclaims everything)
- Secrets in memory — **gone** (no persistence between requests)
This is safer than php-fpm, which reuses workers across requests and relies on
`pm.max_requests` to periodically recycle them. With Qbix Server, every request
gets a clean process. The fork cost (~0.5ms) is negligible compared to the
bootstrap savings (~10– 50ms).
### In-process mode (--workers=0)
Without `--workers` , PHP scripts run directly in the event loop. This is
single-threaded — fine for development and lightweight APIs. Superglobals
(`$_GET` , `$_POST` , etc.) are reset between requests, but static class
variables persist (same as php-fpm). Use `--workers=N` in production for
full isolation.
2026-07-20 10:26:59 -04:00
---
## ⚙️ 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 |
---
## 📦 Three Ways to Run
### 1. From source (needs PHP 8.1+)
```bash
2026-07-20 10:54:19 -04:00
php qbixserver.php --root= ./web --port= 8080
2026-07-20 10:26:59 -04:00
```
### 2. PHAR — single 196KB file (needs PHP)
```bash
2026-07-20 10:54:19 -04:00
php bin/qbixserver.phar --root= ./web --port= 8080
2026-07-20 10:26:59 -04:00
# Or make it executable
2026-07-20 10:54:19 -04:00
chmod +x bin/qbixserver.phar
./bin/qbixserver.phar --port= 8080
2026-07-20 10:26:59 -04:00
```
### 3. Static binary — no PHP needed
```bash
# Download from GitHub Releases
2026-07-20 10:54:19 -04:00
chmod +x qbixserver-linux-x86_64
./qbixserver-linux-x86_64 --root= ./web --port= 8080
2026-07-20 10:26:59 -04:00
```
The binary bundles PHP 8.3 + extensions into a single ~15MB executable.
2026-07-20 10:47:22 -04:00
Copy it to any Linux or macOS machine and run. No dependencies.
2026-07-20 10:26:59 -04:00
---
## 🔨 Building
### Build the PHAR
```bash
php -d phar.readonly= 0 build-phar.php
2026-07-20 10:54:19 -04:00
# Output: bin/qbixserver.phar
2026-07-20 10:26:59 -04:00
```
### Build the static binary
```bash
# With Docker (easiest):
./build-binary.sh --docker
# With static-php-cli installed locally:
./build-binary.sh
2026-07-20 10:54:19 -04:00
# Output: bin/qbixserver (~15MB)
2026-07-20 10:26:59 -04:00
```
The binary is built using [static-php-cli ](https://github.com/crazywhalecc/static-php-cli ),
which compiles PHP + extensions into a statically linked binary.
2026-07-20 10:47:22 -04:00
GitHub Actions automatically builds binaries for **Linux x86_64** , **Linux ARM64** ,
**macOS x86_64** , and **macOS Apple Silicon** on every tagged release.
2026-07-20 10:26:59 -04:00
---
## 🔌 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
2026-07-20 10:54:19 -04:00
php qbixserver.php --app= /path/to/myapp --port= 8080
2026-07-20 10:26:59 -04:00
```
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
```
2026-07-20 10:47:22 -04:00
┌──────────────────┐
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 │ │ │
└──────────┘ └──────────┘ └──────────┘
2026-07-20 10:26:59 -04:00
```
**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.
2026-07-20 10:47:22 -04:00
Workers are forked after class preloading, so they share the base memory footprint
via copy-on-write pages.
2026-07-20 10:26:59 -04:00
2026-07-20 10:47:22 -04:00
**The remaining gap** versus nginx (55– 73%) is inherent: nginx uses
2026-07-20 10:26:59 -04:00
`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
2026-07-20 10:47:22 -04:00
userspace, and every operation has interpreter overhead. Getting to 55– 73% of C
2026-07-20 10:26:59 -04:00
performance from pure interpreted PHP is about as good as it gets.
---
2026-07-20 10:50:05 -04:00
## 🌐 HTTP/2 Support
The built-in event loop uses `stream_select` — zero dependencies, works everywhere.
But if you install [amphp ](https://amphp.org/ ), the server upgrades to a full
HTTP/2 server with no code changes:
```bash
composer require amphp/http-server amphp/socket
2026-07-20 10:54:19 -04:00
php qbixserver.php --port= 8443
2026-07-20 10:50:05 -04:00
```
The server detects amphp automatically and switches to its event loop and HTTP
driver. You get:
| | HTTP/1.1 (built-in) | HTTP/2 (amphp) |
|---|---|---|
| Connections per page load | ~6 parallel | 1 multiplexed |
| Header overhead | Full headers per request | HPACK compressed |
| Event loop | `stream_select` (portable) | `epoll` /`kqueue` via Revolt |
| TLS | `stream_socket_enable_crypto` | amphp native TLS |
| Server push | No | Yes (push static assets before browser asks) |
### How it works
The server has a clean two-layer architecture. `Q_WebServer::route()` handles
all request logic (static files, PHP dispatch, cache, access control) and returns
a `[status, headers, body]` array. The transport layer is pluggable:
```
Built-in: stream_select → accept → fread → route() → fwrite
amphp: Revolt loop → amphp HTTP server → route() → amphp response
```
All the server's features — response cache, X-Accel-Redirect, component cache
invalidation, keep-alive, compression — work identically on both transports.
The `Q_Evented` facade abstracts the event loop, so timers, signals, and socket
watchers work the same way whether you're on `stream_select` or Revolt.
### When to use which
**Built-in (default):** Zero dependencies. Works on any PHP 8.1+ installation.
Good for development, small-to-medium sites, and environments where you can't
install Composer packages.
**amphp:** Better performance under high concurrency thanks to `epoll` /`kqueue` .
HTTP/2 multiplexing reduces connection overhead for asset-heavy pages.
Required if you need server push or HTTP/2-only clients.
**Either way:** You can always put Cloudflare, CloudFront, or nginx in front
as a reverse proxy. The CDN terminates HTTP/2 (and HTTP/3) for you, forwarding
HTTP/1.1 to the backend. In that configuration, the built-in transport is all
you need — the CDN handles the protocol upgrade.
---
2026-07-20 10:26:59 -04:00
## 📋 Requirements
2026-07-20 10:54:19 -04:00
**For qbixserver.php and PHAR:**
2026-07-20 10:26:59 -04:00
- 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 ).
2026-07-20 11:50:30 -04:00
Part of the [Qbix Platform ](https://github.com/Qbix/Platform ).