WebSocket Chat
This example shows how to build a small realtime chat server with the Vix WebSocket module.
It demonstrates:
WebSocket server startup
client connection callbacks
raw text messages
typed JSON messages
rooms
room broadcasts
HTTP health route
shared RuntimeExecutor
configuration from .envUse this example when you want to build realtime features such as:
chat
notifications
live dashboards
collaboration
presence
room-based eventsWhat this example builds
The app runs:
HTTP server
http://127.0.0.1:8080
WebSocket server
ws://127.0.0.1:9090The HTTP server exposes:
GET /
GET /api/healthThe WebSocket server handles:
client connected
client disconnected
raw text message
typed JSON chat.message
typed JSON room.join
typed JSON room.leaveProject structure
Create:
websocket_chat_demo/
├── .env
├── websocket_chat.cpp
└── public/
└── index.html.env
Create:
.envAdd:
APP_NAME=websocket-chat-demo
APP_ENV=development
SERVER_HOST=0.0.0.0
SERVER_PORT=8080
SERVER_REQUEST_TIMEOUT=5000
SERVER_IO_THREADS=0
SERVER_SESSION_TIMEOUT_SEC=20
SERVER_BENCH_MODE=false
WEBSOCKET_HOST=0.0.0.0
WEBSOCKET_PORT=9090
WEBSOCKET_MAX_MESSAGE_SIZE=65536
WEBSOCKET_IDLE_TIMEOUT=60
WEBSOCKET_ENABLE_DEFLATE=true
WEBSOCKET_PING_INTERVAL=30
WEBSOCKET_AUTO_PING_PONG=true
PUBLIC_PATH=public
PUBLIC_MOUNT=/
PUBLIC_INDEX=index.html
PUBLIC_CACHE_CONTROL=no-cache
PUBLIC_SPA_FALLBACK=falseThe important WebSocket values are:
WEBSOCKET_HOST
WEBSOCKET_PORT
WEBSOCKET_MAX_MESSAGE_SIZE
WEBSOCKET_IDLE_TIMEOUT
WEBSOCKET_ENABLE_DEFLATE
WEBSOCKET_PING_INTERVAL
WEBSOCKET_AUTO_PING_PONGpublic/index.html
Create:
public/index.htmlAdd:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Vix WebSocket Chat</title>
<style>
body {
margin: 0;
font-family: system-ui, sans-serif;
background: #f6f7f9;
color: #111827;
}
.page {
max-width: 760px;
margin: 60px auto;
padding: 24px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 16px;
}
.row {
display: flex;
gap: 8px;
margin-top: 16px;
}
input {
flex: 1;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 10px;
}
button {
padding: 10px 14px;
border: 0;
border-radius: 10px;
cursor: pointer;
}
pre {
min-height: 260px;
margin-top: 20px;
padding: 16px;
background: #111827;
color: #f9fafb;
border-radius: 12px;
overflow: auto;
white-space: pre-wrap;
}
</style>
</head>
<body>
<main class="page">
<h1>Vix WebSocket Chat</h1>
<p>Open this page in two browser tabs and send messages.</p>
<div class="row">
<input id="room" value="general" placeholder="room" />
<button id="join">Join room</button>
</div>
<div class="row">
<input id="message" placeholder="message" />
<button id="send">Send</button>
</div>
<pre id="log"></pre>
</main>
<script>
const log = document.querySelector("#log");
const roomInput = document.querySelector("#room");
const messageInput = document.querySelector("#message");
const joinButton = document.querySelector("#join");
const sendButton = document.querySelector("#send");
let currentRoom = "general";
function write(line) {
log.textContent += line + "\n";
log.scrollTop = log.scrollHeight;
}
const socket = new WebSocket("ws://127.0.0.1:9090/");
socket.addEventListener("open", () => {
write("connected");
socket.send(
JSON.stringify({
type: "room.join",
payload: {
room: currentRoom,
},
}),
);
});
socket.addEventListener("message", (event) => {
write("server: " + event.data);
});
socket.addEventListener("close", () => {
write("closed");
});
socket.addEventListener("error", () => {
write("error");
});
joinButton.addEventListener("click", () => {
currentRoom = roomInput.value || "general";
socket.send(
JSON.stringify({
type: "room.join",
payload: {
room: currentRoom,
},
}),
);
write("joined room: " + currentRoom);
});
sendButton.addEventListener("click", () => {
const text = messageInput.value;
if (!text) {
return;
}
socket.send(
JSON.stringify({
type: "chat.message",
payload: {
room: currentRoom,
text: text,
},
}),
);
messageInput.value = "";
});
</script>
</body>
</html>This browser page connects to:
ws://127.0.0.1:9090/and sends typed JSON messages.
websocket_chat.cpp
Create:
websocket_chat.cppAdd:
#include <memory>
#include <string>
#include <vix.hpp>
#include <vix/middleware.hpp>
#include <vix/websocket.hpp>
#include <vix/json.hpp>
#include <vix/print.hpp>
using namespace vix;
static std::string payload_string(
const vix::json::kvs &payload,
const std::string &key,
const std::string &fallback)
{
auto it = payload.find(key);
if (it == payload.end())
return fallback;
return it->second;
}
static void register_http(vix::App &app)
{
app.static_dir(
"public",
"/",
"index.html",
true,
"no-cache",
true,
false
);
app.get("/api/health", [](vix::Request &, vix::Response &res)
{
res.json({
"ok", true,
"service", "websocket-chat"
});
});
}
static void register_websocket_handlers(vix::websocket::Server &ws)
{
ws.on_open([](vix::websocket::Session &session)
{
session.send_text("welcome to Vix WebSocket chat");
vix::print("client connected");
});
ws.on_close([](vix::websocket::Session &session)
{
(void)session;
vix::print("client disconnected");
});
ws.on_error(
[](vix::websocket::Session &session, const std::string &error)
{
(void)session;
vix::print("websocket error:", error);
});
ws.on_message(
[](vix::websocket::Session &session, const std::string &message)
{
vix::print("raw message:", message);
if (message == "ping")
{
session.send_text("pong");
}
});
ws.on_typed_message(
[&ws](vix::websocket::Session &session,
const std::string &type,
const vix::json::kvs &payload)
{
if (type == "room.join")
{
const std::string room =
payload_string(payload, "room", "general");
ws.join_room(session.shared_from_this(), room);
session.send_text("joined room: " + room);
return;
}
if (type == "room.leave")
{
const std::string room =
payload_string(payload, "room", "general");
ws.leave_room(session.shared_from_this(), room);
session.send_text("left room: " + room);
return;
}
if (type == "chat.message")
{
const std::string room =
payload_string(payload, "room", "general");
const std::string text =
payload_string(payload, "text", "");
if (text.empty())
{
session.send_text("message rejected: empty text");
return;
}
ws.broadcast_text_to_room(
room,
"room " + room + ": " + text
);
return;
}
session.send_text("unknown message type: " + type);
});
}
int main()
{
vix::config::Config config{".env"};
vix::App app;
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);
vix::websocket::Server ws{config, executor};
register_http(app);
register_websocket_handlers(ws);
vix::websocket::AttachedRuntime runtime{
app,
ws,
executor
};
app.run(config);
return 0;
}Run it
From the project directory:
vix run websocket_chat.cppThe app starts:
HTTP
http://127.0.0.1:8080
WebSocket
ws://127.0.0.1:9090Open the browser:
http://127.0.0.1:8080/Open the page in two browser tabs.
Join the same room and send a message.
Both tabs should receive the room broadcast.
Test HTTP health
curl -i http://127.0.0.1:8080/api/healthExpected body shape:
{
"ok": true,
"service": "websocket-chat"
}Test WebSocket with browser
Open:
http://127.0.0.1:8080/Expected behavior:
browser connects to ws://127.0.0.1:9090/
server sends welcome message
browser joins room general
messages are broadcast to clients in the same roomTest with websocat
If you have websocat installed:
websocat ws://127.0.0.1:9090/Send a raw message:
pingExpected response:
pongSend a typed room join message:
{ "type": "room.join", "payload": { "room": "general" } }Expected response:
joined room: generalSend a typed chat message:
{
"type": "chat.message",
"payload": { "room": "general", "text": "Hello from terminal" }
}Expected response for clients in the room:
room general: Hello from terminalTyped message format
The browser sends messages like this:
{
"type": "chat.message",
"payload": {
"room": "general",
"text": "Hello"
}
}The important fields are:
type
application event name
payload
event dataFor this example, the server handles:
room.join
room.leave
chat.messageRaw messages vs typed messages
The server registers a raw message handler:
ws.on_message(
[](vix::websocket::Session &session, const std::string &message)
{
if (message == "ping")
{
session.send_text("pong");
}
});It also registers a typed message handler:
ws.on_typed_message(
[&ws](vix::websocket::Session &session,
const std::string &type,
const vix::json::kvs &payload)
{
// handle typed JSON events
});Use raw messages for simple protocols.
Use typed messages for real application events.
A chat app should usually use typed messages.
Rooms
Rooms let you group sessions.
The example joins a room with:
ws.join_room(session.shared_from_this(), room);It leaves a room with:
ws.leave_room(session.shared_from_this(), room);It broadcasts to a room with:
ws.broadcast_text_to_room(
room,
"room " + room + ": " + text
);Rooms are useful for:
chat rooms
support conversations
project channels
live dashboards
tenant-specific events
product pagesWhy shared_from_this() is used
Room APIs work with a shared session pointer.
Inside callbacks, the server gives you:
vix::websocket::Session &sessionTo join a room, use:
session.shared_from_this()Example:
ws.join_room(session.shared_from_this(), "general");The server owns active sessions and room membership by shared session references.
Attached runtime
This example uses:
vix::websocket::AttachedRuntime runtime{
app,
ws,
executor
};This connects:
vix::App
vix::websocket::Server
RuntimeExecutorThe HTTP app and WebSocket server run as one application lifecycle.
The WebSocket server starts before the HTTP app enters app.run(...).
When the app shuts down, the runtime coordinates WebSocket shutdown safely.
Why use a shared executor
The WebSocket server needs a runtime executor:
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);Then:
vix::websocket::Server ws{config, executor};and:
vix::websocket::AttachedRuntime runtime{
app,
ws,
executor
};This keeps HTTP and WebSocket runtime coordination explicit.
WebSocket configuration
The server reads configuration from:
vix::config::Config config{".env"};Important .env values:
WEBSOCKET_HOST=0.0.0.0
WEBSOCKET_PORT=9090
WEBSOCKET_MAX_MESSAGE_SIZE=65536
WEBSOCKET_IDLE_TIMEOUT=60
WEBSOCKET_ENABLE_DEFLATE=true
WEBSOCKET_PING_INTERVAL=30
WEBSOCKET_AUTO_PING_PONG=trueThe browser connects to the configured port:
const socket = new WebSocket("ws://127.0.0.1:9090/");If you change WEBSOCKET_PORT, update the browser URL too.
HTTP and WebSocket ports
In this example:
HTTP server
8080
WebSocket server
9090That means:
browser page
http://127.0.0.1:8080/
WebSocket connection
ws://127.0.0.1:9090/In production, you may place both behind a reverse proxy.
The example keeps them separate to make the architecture clear.
Avoid blocking inside callbacks
Do not run long blocking work directly inside WebSocket callbacks.
Avoid this:
ws.on_message([](auto &, const std::string &)
{
std::this_thread::sleep_for(std::chrono::seconds(10));
});Callbacks should stay small.
For expensive work, dispatch to another service, queue, job system, or runtime task.
Error handling
The example logs WebSocket errors:
ws.on_error(
[](vix::websocket::Session &session, const std::string &error)
{
(void)session;
vix::print("websocket error:", error);
});Use this for:
connection diagnostics
invalid frames
client disconnect issues
read failures
write failuresSecurity notes
This is a demo.
For a real chat system, add:
authentication
room authorization
message size limits
rate limiting
input validation
message persistence
presence state
origin checks
TLS through reverse proxyDo not allow any client to join any room in production unless that is your intended design.
Common mistakes
Using HTTP port for WebSocket
This example uses:
HTTP 8080
WebSocket 9090So this is correct:
new WebSocket("ws://127.0.0.1:9090/");This is wrong for this example:
new WebSocket("ws://127.0.0.1:8080/");unless you configured WebSocket to run on the same port through another integration.
Forgetting the executor
The server needs a runtime executor:
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);
vix::websocket::Server ws{config, executor};Blocking shutdown incorrectly
Do not manually block inside shutdown callbacks.
Use AttachedRuntime to coordinate HTTP and WebSocket lifecycle.
Treating raw messages as typed messages
Raw messages are plain strings.
Typed messages are JSON objects with:
type
payloadUse on_typed_message(...) for typed events.
Complete test flow
Run:
vix run websocket_chat.cppOpen:
http://127.0.0.1:8080/Open two browser tabs.
In both tabs:
room = general
click Join roomIn one tab, send:
Hello from VixExpected behavior:
both clients in the general room receive the messageTest raw ping with websocat:
websocat ws://127.0.0.1:9090/Send:
pingExpected response:
pongSummary
A minimal Vix WebSocket chat needs:
vix::config::Config config{".env"};
auto executor =
std::make_shared<vix::executor::RuntimeExecutor>(4);
vix::websocket::Server ws{config, executor};Then register callbacks:
ws.on_open(...);
ws.on_close(...);
ws.on_error(...);
ws.on_message(...);
ws.on_typed_message(...);Use rooms for grouped broadcasts:
ws.join_room(session.shared_from_this(), "general");
ws.broadcast_text_to_room("general", "message");Use AttachedRuntime when HTTP and WebSocket run together:
vix::websocket::AttachedRuntime runtime{
app,
ws,
executor
};The mental model is:
vix::App
serves HTTP routes and static files
vix::websocket::Server
handles realtime connections
AttachedRuntime
coordinates lifecycle
rooms
group sessions
typed messages
carry application events