
Phlo Realtime: WebSockets without leaving the language
19 Mar 2026
Most frameworks treat realtime as a second stack bolted onto the first: a separate WebSocket server, a separate event bus, a separate mental model for "the stuff that pushes." Phlo Realtime is built to be additive instead. A Phlo app is a normal request-response app until the Phlo Daemon runs alongside it, and only then does the realtime layer light up.
One process, many apps
The daemon owns the socket connections. It is a single process that multiplexes WebSocket connections across every app configured on the machine, and dispatches each event (connect, receive, close) to the right app as a fresh, clean invocation on a worker pool. You never write a socket loop; you implement four hooks and the daemon calls them.
method wsConnect($conn) => %session->user ? true : false
method wsAuth($conn, $token) => user::record(token: $token)
method wsReceive($conn, $data) => wsCast('poll:'.$conn->room, votes: poll::tally($conn->room))
method wsClose($conn) => void
wsConnect decides whether a connection is allowed in at all. wsAuth resolves who is on the other end from a token, if you need identity. wsReceive handles an incoming message. wsClose cleans up when a client disconnects. None of this asks you to learn a new runtime: these are Phlo methods, called like any route.
Broadcasting is one call
The other half is pushing from plain PHP. wsCast() broadcasts a payload to every connection subscribed to a channel, from anywhere in your app, including a completely unrelated HTTP request:
method vote($option){
poll::castVote($option)
wsCast('poll:'.$this->room, votes: poll::tally($this->room))
}
A normal POST /vote handler runs, updates the database, and pushes the new tally to every open tab watching that poll, in the same request. There is no separate publish/subscribe client to configure and no message format to invent; the daemon and the app already speak the same protocol.
Stateless by design
The daemon's worker pool is where the sockets live, but the data stays in the database, not in daemon memory. Every event runs as a fresh invocation against the current state, the same way an HTTP request would. That keeps a hard problem out of the picture entirely: you never reconcile "what does this long-lived connection think the state is" against "what the database actually says," because the connection never holds state of its own to drift.
Try it end to end
The Learn tutorial builds a live poll from scratch in eight steps: install, a route, a view, then wiring up exactly these four hooks, ending in a poll where a vote in one tab updates every other open tab immediately. It is the fastest way to see the whole loop, connect through broadcast, without reading a spec first.