3: The dispatch API
The daemon's main endpoint is POST /dispatch. It binds 127.0.0.1 by default, so it is local-only; gate it at the network boundary.
3.1: Routing: by app path
A dispatch carries {app, target, args?, stream?, async?}: the absolute .../app.php path tells the daemon which app to run, and each app's pool is keyed by that path. The runtime helpers (enabled by the daemon constant) know their own app and dispatch directly; there is no host map to consult.
The built-in WebSocket server (Phlo Realtime) does not go through /dispatch. It only knows a connection's Host header, so it resolves that to an app through the registry, which is populated from the hosts map in config/daemon.js, then dispatches the websocket::<hook> target in-process on the same pool. POST /message (the broadcast bridge) and GET /health complete the API.
3.2: Sync, async, stream
The same endpoint answers in three shapes:
| Request | Response |
|---|---|
| default | {status:"ok", result} once the call returns |
async: true |
202 {status:"ok", queued:true} immediately; the call runs fire-and-forget on the pool |
stream: true |
an application/x-ndjson stream of {t:line,data}* then {t:done,result} or {t:error} |
Streaming is how progressive output (a Phlo Realtime receive handler, an AI token stream) flows back line by line as the worker prints it.
3.3: What comes back
The pool returns the target's real return value, typed (a boolean stays a boolean). The one-shot fallback returns the process's stdout as a string, so a consumer that branches on the result handles both shapes. Errors surface as a rejected dispatch (HTTP error for sync, a {t:error} frame for streams), which is how a thrown handler becomes a refused WebSocket handshake.