Example Apps

Six apps included in the repo. Each one runs with a single command.

Every example demonstrates a different server capability. Clone the repo, run the command, open your browser. No Docker, no npm install, no configuration files.

git clone https://github.com/Qbix/webserver
cd webserver
💬 Chat — Real-time multi-room chat
php qbixserver.php --root=examples/chat/web

WebSocket rooms with typing indicators, read receipts, emoji reactions, and presence. Each room is a forked PHP process with shared state. The server handles all the Socket.IO protocol — no Node.js, no Redis pub/sub.

How a message gets broadcast — one PHP file:

// handlers/chat/message.php
function chat_message($params) {
    $room = $params['room'];
    $data = $params['data'] ?? [];
    $socketId = $room->senderId();

    $room->state['messageCount']++;

    Q_Socket::broadcastExcept([
        'message', [
            'name' => $data['name'] ?? 'anon',
            'text' => $data['text'] ?? '',
            'time' => date('H:i'),
        ]
    ], $socketId);
}

That's the entire server-side message handler. The room process holds state in $room->state — no database, no Redis. When the room process dies, everything is reclaimed.

8 handler files: init, join, leave, message, typing, stop_typing, reaction, read

View on GitHub →

📋 Collab — Real-time kanban board
php qbixserver.php --root=examples/collab/web

Collaborative kanban board with drag-and-drop cards, live remote cursors, and server-authoritative state. The board state lives in the room process — new users get the full board on join.

Adding a card — the server validates and broadcasts:

// handlers/board/card_add.php
function board_card_add($params) {
    $room = $params['room'];
    $data = $params['data'];
    
    $room->state['nextId']++;
    $card = [
        'id'     => $room->state['nextId'],
        'text'   => $data['text'],
        'column' => $data['column'] ?? 'todo',
        'author' => $data['name'] ?? 'anon'
    ];
    $room->state['cards'][] = $card;

    // Everyone sees the new card instantly
    Q_Socket::broadcast(['card_added', $card]);
}

Server-authoritative: the room process holds the canonical card list. No conflicts, no CRDTs needed.

9 handler files: init, join, leave, card_add, card_move, card_edit, card_delete, cursor, sync

View on GitHub →

⚡ Stream — Server-Sent Events (SSE)
php qbixserver.php --root=examples/stream/web

Four SSE streaming modes: AI token streaming, server log tail, structured JSON data, and a simple counter. Demonstrates how the server auto-detects SSE from Content-Type: text/event-stream.

The entire streaming endpoint:

// web/stream.php
Q_Response::header('Content-Type: text/event-stream');
Q_Response::header('Cache-Control: no-cache');

$words = explode(' ', 'The Qbix Server is a pure PHP web server...');
foreach ($words as $word) {
    echo "data: $word\n\n";
    ob_flush(); flush();
    usleep(80000); // 80ms between tokens
}

Each ob_flush() sends a chunked frame to the browser immediately. No buffering, no polling. The same pattern works for streaming AI responses, log tails, or live sensor data.

View on GitHub →

✅ Todo — REST API with SQLite
php qbixserver.php --root=examples/todo/web

Full CRUD todo list. SQLite database auto-creates on first run. JSON API with proper HTTP verbs. No framework, no ORM — just PHP and SQLite.

The API is one file:

// web/api.php
Q_Response::header('Content-Type: application/json');
$db = new SQLite3('../data/todos.db');
$input = json_decode(file_get_contents('php://input'), true) ?: [];

switch ($_SERVER['REQUEST_METHOD']) {
    case 'GET':
        $rows = [];
        $stmt = $db->query('SELECT * FROM todos ORDER BY id DESC');
        while ($r = $stmt->fetchArray(SQLITE3_ASSOC)) $rows[] = $r;
        echo json_encode($rows);
        break;
    case 'POST':
        $stmt = $db->prepare('INSERT INTO todos (text) VALUES (:t)');
        $stmt->bindValue(':t', $input['text']);
        $stmt->execute();
        echo json_encode(['id' => $db->lastInsertRowID(), 'ok' => true]);
        break;
}

No database server to install. SQLite in WAL mode handles 50,000+ writes/sec. The database file survives server restarts.

View on GitHub →

🔢 Counter — Live page views
php qbixserver.php --root=examples/counter/web

Live page view counter with animated viewer dots. SQLite for persistence, WebSocket heartbeat for real-time updates across tabs.

The counter endpoint:

// web/count.php
$db = new SQLite3('../data/counter.db');
$db->exec('CREATE TABLE IF NOT EXISTS counter (id INTEGER PRIMARY KEY, count INTEGER DEFAULT 0)');
$db->exec('INSERT OR IGNORE INTO counter (id, count) VALUES (1, 0)');

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $db->exec('UPDATE counter SET count = count + 1 WHERE id = 1');
}
$count = $db->querySingle('SELECT count FROM counter WHERE id = 1');
echo json_encode(['count' => (int) $count]);

The browser POSTs on page load to increment, then opens a WebSocket to /Q/ws for live stats. Every connected tab sees updates instantly.

View on GitHub →

🐝 Swarm — Distributed tasks + microservice isolation
# Single server
php qbixserver.php --root=examples/swarm/web --port=4001

# Or: authority + sandbox (microservice isolation)
php qbixserver.php --root=examples/swarm/web --config=examples/swarm/config/authority.json
php qbixserver.php --root=examples/swarm/web --config=examples/swarm/config/sandbox.json

# Or: multi-node cluster
PEERS=http://localhost:4001 php qbixserver.php --root=examples/swarm/web --port=4002

The most complete example. Distributed task list + chat with cluster replication and authority/sandbox isolation. Demonstrates three things at once:

1. Cluster replication — events replicate to peers automatically:

// handlers/swarm/task_add.php runs locally, then the server
// broadcasts to all peers via Q_WebServer_Cluster
function swarm_task_add($params) {
    $data = $params['data'];
    $uuid = bin2hex(random_bytes(8));
    // ... store in local SQLite ...
    return ['ok' => true, 'uuid' => $uuid];
}
// Config says this event replicates:
// "Q.cluster.replicate": ["swarm/task_add", "swarm/task_update", ...]

2. Microservice isolation — same code, different config:

// sandbox.json — the public-facing server
// Forwards sensitive operations to the authority:
{
  "Q": {
    "handlersUsingRemote": {
      "swarm/task_add":  {"baseUrl": "http://localhost:4001"},
      "swarm/payment":   {"baseUrl": "http://localhost:4001"}
    }
  }
}

// authority.json — has the secrets
// Runs the same code but with payment keys, SMTP, etc.
{
  "Q": {
    "internal": {
      "paymentApiKey": "sk_live_abc123",
      "smtp": {"host": "smtp.mailgun.org", "pass": "..."}
    }
  }
}

The sandbox never loads payment handlers, never sees the secrets. If it gets compromised, the attacker gets an HMAC key that can only make the same API calls the sandbox already makes.

3. Self-healing sync — kill a node, restart it, it catches up from peers automatically via /api.php?action=sync.

View on GitHub →

Run them all

git clone https://github.com/Qbix/webserver
cd webserver

php qbixserver.php --root=examples/chat/web --port=8001 &
php qbixserver.php --root=examples/todo/web --port=8002 &
php qbixserver.php --root=examples/stream/web --port=8003 &
php qbixserver.php --root=examples/counter/web --port=8004 &
php qbixserver.php --root=examples/collab/web --port=8005 &
php qbixserver.php --root=examples/swarm/web --port=8006 &

Six apps running simultaneously on one machine. Each gets its own persistent workers, auto-detected from your CPU count. Total RAM: under 100MB.