Realtime
Cnerium realtime is the application-level event API exposed by the attached Cnerium layer.
It is used to emit events after durable operations succeed. The WebSocket transport itself belongs to Vix. Cnerium does not implement a separate WebSocket server, session model, room system, frame parser, heartbeat, or low-level connection lifecycle.
A typical event emission looks like this:
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)}
}));The event says what happened at the application level. Vix handles the realtime transport.
Header
#include <cnerium/cnerium.hpp>or directly:
#include <cnerium/realtime/Realtime.hpp>
#include <cnerium/realtime/Event.hpp>
#include <cnerium/realtime/EventPayload.hpp>
#include <cnerium/realtime/RealtimeConfig.hpp>Most applications should include:
#include <vix.hpp>
#include <cnerium/cnerium.hpp>Namespace
Realtime types live under:
namespace cnerium::realtimeCommon public aliases are also available in:
namespace cneriumCommon types include:
cnerium::Event
cnerium::EventPayload
cnerium::realtime::RealtimeConfigMost application code emits events through cnerium::AttachedApp:
auto cnerium = cnerium::attach(app);
cnerium.emit("order.created", payload);Purpose
Realtime support exists so durable operations can notify connected clients when something important happens.
For example, after a durable order route creates an order, it can emit:
order.createdThat event can update a dashboard, notify a browser client, or inform a live UI that new state exists.
The event is tied to the durable handler execution. If the same request is safely retried and Cnerium returns the stored response, the handler does not run again. That means the event is not emitted again by that handler.
This behavior is important because it avoids duplicate realtime notifications caused by HTTP retries.
Boundary with Vix
Cnerium does not own WebSocket infrastructure.
Vix owns:
WebSocket server
connection lifecycle
sessions
rooms
message transport
runtime executor
low-level protocol behaviorCnerium owns:
application event type
application event payload
emit
emit_to
durable-operation event timingThe boundary is:
Cnerium says what happened.
Vix delivers it.This keeps Cnerium inside the Vix ecosystem instead of creating a second realtime framework.
Enable realtime
Realtime is enabled through AppConfig before attaching Cnerium:
vix::App app;
cnerium::app::AppConfig config =
cnerium::app::AppConfig::development();
config.set_name("orders-service");
config.set_data_dir("data/cnerium");
config.set_node_id("orders-node");
config.enable_realtime("/ws", "0.0.0.0", 9090);
auto cnerium =
cnerium::attach(app, std::move(config));The call:
config.enable_realtime("/ws", "0.0.0.0", 9090);sets the public endpoint, bind host, and port used by the realtime layer.
Configure realtime after attach
If the current API exposes the convenience method on AttachedApp, realtime can also be configured through the attached object:
auto cnerium = cnerium::attach(app);
cnerium.realtime("/ws", "0.0.0.0", 9090);This is equivalent in intent to enabling realtime through AppConfig.
Use the explicit AppConfig form when you want all Cnerium runtime settings in one place.
Start realtime resources
Realtime resources are started with the Cnerium layer:
if (!cnerium.start())
{
return 1;
}
app.run();cnerium.start() prepares Cnerium resources such as storage and optional realtime support.
app.run() starts the Vix HTTP application.
Cnerium does not run the Vix application for you.
RealtimeConfig
cnerium::realtime::RealtimeConfig stores the realtime settings used by Cnerium.
Common values are:
enabled
endpoint
host
portA typical enabled configuration is:
auto realtime =
cnerium::realtime::RealtimeConfig::enabled(
"/ws",
"0.0.0.0",
9090);A disabled configuration is:
auto realtime =
cnerium::realtime::RealtimeConfig::disabled();Most application code should use AppConfig::enable_realtime(...) rather than constructing RealtimeConfig manually.
Event
cnerium::realtime::Event represents one application-level realtime event.
An event has:
type
payloadExample:
cnerium::Event event{
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)}
})};The event type should describe a completed fact:
order.created
payment.created
invoice.created
user.registered
workflow.startedAvoid vague event names:
created
done
success
update
eventEvent names become part of the application contract, so they should remain stable.
EventPayload
cnerium::EventPayload is the payload carried by a realtime event.
It uses Cnerium JSON, backed by Vix JSON:
cnerium::EventPayload payload =
cnerium::support::object({
{"order_id", cnerium::Json(order_id)},
{"status", cnerium::Json("created")}
});A good payload contains enough information for the receiver to understand what changed:
{
"order_id": "ord_order-123",
"status": "created"
}Keep event payloads focused. Realtime events should notify clients that something happened. They should not replace the application’s data API.
emit
emit sends an event to connected clients.
Common usage:
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)}
}));Common signatures on the attached app are:
bool emit(const cnerium::realtime::Event &event);bool emit(
std::string type,
cnerium::realtime::EventPayload payload = cnerium::support::object());emit returns true if the event was emitted by the current runtime state.
If realtime is disabled or not running, emission may fail. Application code should decide whether that failure matters for the operation.
For most durable HTTP operations, realtime delivery should be treated as a notification layer, not as the source of truth.
emit_to
emit_to sends an event to a room.
Example:
cnerium.emit_to(
"orders",
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)}
}));Common signatures are:
bool emit_to(
const std::string &room,
const cnerium::realtime::Event &event);bool emit_to(
const std::string &room,
std::string type,
cnerium::realtime::EventPayload payload = cnerium::support::object());Use emit_to when only a subset of connected clients should receive the event.
Room names should be stable and meaningful:
orders
admin
shop:42
user:123The underlying room delivery belongs to the Vix WebSocket runtime.
Emit from a durable route
The most common use is to emit after a durable operation succeeds:
cnerium.durable_post(
"/orders",
"orders.create",
[&cnerium](cnerium::DurableRequest &request)
{
const auto body = request.json();
const std::string product_id =
cnerium::support::string_or(body, "product_id", "");
const int quantity =
cnerium::support::int_or(body, "quantity", 0);
if (product_id.empty())
{
return cnerium::DurableResponse::bad_request(
"Missing required field: product_id");
}
if (quantity <= 0)
{
return cnerium::DurableResponse::bad_request(
"Field quantity must be greater than zero");
}
const std::string order_id =
"ord_" + request.idempotency_key_value();
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)},
{"product_id", cnerium::Json(product_id)},
{"quantity", cnerium::Json(quantity)}
}));
return cnerium::created({
{"ok", true},
{"order_id", order_id},
{"product_id", product_id},
{"quantity", quantity}
});
});The event is emitted only when the handler executes.
If Cnerium replays a stored response, the handler is not called again, so the event is not emitted again by that handler.
Complete example
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
#include <string>
#include <utility>
int main()
{
vix::App app;
cnerium::app::AppConfig config =
cnerium::app::AppConfig::development();
config.set_name("orders-service");
config.set_data_dir("data/cnerium");
config.set_node_id("orders-node");
config.enable_realtime("/ws", "0.0.0.0", 9090);
auto cnerium =
cnerium::attach(app, std::move(config));
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders-service"}
});
});
cnerium.durable_post(
"/orders",
"orders.create",
[&cnerium](cnerium::DurableRequest &request)
{
const auto body = request.json();
const std::string product_id =
cnerium::support::string_or(body, "product_id", "");
const int quantity =
cnerium::support::int_or(body, "quantity", 0);
if (product_id.empty())
{
return cnerium::DurableResponse::bad_request(
"Missing required field: product_id");
}
if (quantity <= 0)
{
return cnerium::DurableResponse::bad_request(
"Field quantity must be greater than zero");
}
const std::string order_id =
"ord_" + request.idempotency_key_value();
const bool emitted =
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)},
{"product_id", cnerium::Json(product_id)},
{"quantity", cnerium::Json(quantity)}
}));
(void)emitted;
return cnerium::created({
{"ok", true},
{"order_id", order_id},
{"product_id", product_id},
{"quantity", quantity}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}This example keeps Vix as the application owner, attaches Cnerium, enables realtime, and emits an event after a durable order operation.
Event timing
An event should usually be emitted after the operation has succeeded.
Recommended flow:
validate request
perform the operation
emit event
return DurableResponseFor example:
const Order order =
create_order(...);
cnerium.emit(
"order.created",
payload);
return cnerium::created({
{"ok", true},
{"order_id", order.id}
});Do not emit success events before validation passes or before the operation has actually completed.
Retry behavior
Realtime events are tied to handler execution.
The expected behavior is:
new request
handler executes
event is emitted
response is stored
same key with same body
stored response is returned
handler does not execute
event is not emitted again by the handler
same key with different body
409 Conflict
handler does not execute
event is not emitted by the handler
missing Idempotency-Key
400 Bad Request
handler does not execute
event is not emitted by the handlerThis is one of the main reasons to emit events from inside durable handlers.
Response versus event
The durable response is the result of the HTTP request.
The realtime event is a notification.
For example:
POST /orders response
tells the caller that the order was created
order.created event
tells connected clients that an order was createdThe response should contain the data the caller needs. The event should contain enough data for clients to update or fetch the changed state.
Do not make event delivery the only proof that the operation happened.
Event delivery failure
emit returns a boolean.
const bool emitted =
cnerium.emit("order.created", payload);For most applications, a failed realtime emission should not undo the durable operation.
A practical pattern is:
const bool emitted =
cnerium.emit("order.created", payload);
(void)emitted;
return cnerium::created({
{"ok", true},
{"order_id", order_id}
});If event delivery is business-critical, use a durable event log or job system in addition to realtime notification.
Cnerium realtime events are not a durable queue.
Realtime is not persistence
Realtime events should not be used as the only place where application state exists.
For an order route, store the order in the application database or domain store. Emit the event as a notification.
A client that misses the event should still be able to fetch state through a normal Vix route.
Realtime is not a job system
Cnerium realtime events are not a background job queue, worker system, or guaranteed delivery mechanism.
Use them for live notifications and UI updates.
For background processing, retries, dead-letter behavior, or guaranteed asynchronous execution, use a dedicated job or workflow system.
Testing
Start the application, then send a new durable request:
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{"product_id":"p1","quantity":2}'Expected behavior:
HTTP 201 Created
handler runs
order.created is emitted
response is storedRetry the same request:
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{"product_id":"p1","quantity":2}'Expected behavior:
same stored response
handler does not run
order.created is not emitted again by the handlerReuse the key with a different body:
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-123" \
-d '{"product_id":"p2","quantity":1}'Expected behavior:
HTTP 409 Conflict
handler does not run
order.created is not emittedCommon mistakes
Do not treat Cnerium realtime as a replacement for Vix WebSocket. Vix owns the transport.
Do not emit success events before the operation has succeeded.
Do not expect an event to emit again when a safe retry is replayed from storage.
Do not make event delivery the source of truth for the operation.
Do not use realtime events as a durable job queue.
Do not emit large payloads when a small event plus a normal fetch route would be cleaner.
Summary
Cnerium realtime is the event API for durable application operations.
Enable it through AppConfig, start Cnerium resources, emit events from durable handlers, and let Vix handle the WebSocket transport. Safe retries replay stored responses instead of executing handlers again, so events emitted inside handlers are not duplicated by HTTP retries.