Your First Durable Route
This guide builds a small Vix backend and attaches Cnerium to protect one critical POST route.
The goal is not to learn Vix routing from the beginning. Vix already owns the application model. The goal here is to show how Cnerium fits into that model and what changes when a normal write route becomes durable.
You will create:
GET /health
POST /ordersGET /health remains a normal Vix route. POST /orders becomes a Cnerium durable route, protected by an Idempotency-Key.
Create the application
Start with a normal Vix application.
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders"}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}The important part is this line:
auto cnerium = cnerium::attach(app);Cnerium is attached to the existing vix::App. It does not replace it. The HTTP server, routing, middleware, request parsing, response writing, and server lifecycle still belong to Vix.
At this point, the application has no durable route yet. It only proves the attachment model.
Add a durable POST route
Now add POST /orders.
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
return cnerium::created({
{"ok", true}
});
});The first argument is the HTTP path:
/ordersThe second argument is the stable operation name:
orders.createThe operation name is part of Cnerium’s reliability model. It separates this durable operation from other durable operations in the same application. A future payments.create route should not share the same idempotency namespace as orders.create.
The third argument is the durable handler. It receives a cnerium::DurableRequest and returns a cnerium::DurableResponse.
Complete example
This is the complete minimal example:
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders"}
});
});
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
(void)request;
return cnerium::created({
{"ok", true}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}This application can now accept a durable POST /orders request.
Add request parsing
A useful order route should read the JSON body and validate the required fields.
cnerium.durable_post(
"/orders",
"orders.create",
[](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();
return cnerium::created({
{"ok", true},
{"order_id", order_id},
{"product_id", product_id},
{"quantity", quantity}
});
});The handler is only responsible for processing a request that Cnerium has decided is safe to execute.
Cnerium handles the reliability decision before the handler runs. If the request is a safe retry, the stored response is returned and this handler is not executed again. If the request reuses the same idempotency key with a different body, Cnerium returns 409 Conflict.
Full durable orders example
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
#include <string>
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders"}
});
});
cnerium.durable_post(
"/orders",
"orders.create",
[](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();
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 is still a Vix backend. Cnerium is only attached to protect the POST /orders operation.
Run the application
Build and run the project with the normal Vix workflow:
vix build
vix runOr, if you are working inside the Cnerium repository and building the example target directly:
vix build --build-target all -v -- -DCNERIUM_BUILD_EXAMPLES=ON
./build-ninja/cnerium_durable_orders_realtimeWhen the server starts, Vix owns the HTTP runtime and prints the server information.
Send the first request
Send a durable request with an Idempotency-Key header:
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 result:
HTTP/1.1 201 CreatedExample body:
{
"ok": true,
"order_id": "ord_order-123",
"product_id": "p1",
"quantity": 2
}This is the first time Cnerium sees order-123 for the orders.create operation. It executes the handler, stores the request hash, stores the response, and returns the result.
Retry the same request
Send the same request again with the same key and the same 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":"p1","quantity":2}'Expected result:
HTTP/1.1 201 CreatedThe response body should match the first response.
This time, Cnerium does not need to execute the handler again. It finds the stored request hash, sees that the body matches, loads the stored response, and returns it.
That is what makes the route retry-safe.
Reuse the key with a different body
Now reuse the same key with a different payload:
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 result:
HTTP/1.1 409 ConflictExample body:
{
"error": "Idempotency-Key was reused with a different request body"
}This is not a safe retry. The same idempotency key now refers to a different request body. Cnerium rejects it instead of allowing the application to execute an ambiguous operation.
Omit the Idempotency-Key
A durable route requires an idempotency key.
curl -i -X POST http://127.0.0.1:8080/orders \
-H "Content-Type: application/json" \
-d '{"product_id":"p1","quantity":2}'Expected result:
HTTP/1.1 400 Bad RequestThe exact body may depend on the current Cnerium response implementation, but the rule is stable: a durable route cannot process a write operation without an idempotency key.
Add realtime notification
A durable route can emit an application-level event after the operation succeeds.
First enable realtime through the Cnerium configuration:
cnerium.config().enable_realtime("/ws", "0.0.0.0", 9090);Then emit an event from the handler:
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)},
{"product_id", cnerium::Json(product_id)},
{"quantity", cnerium::Json(quantity)}
}));The event is sent through Vix WebSocket. Cnerium does not implement a second WebSocket server. It exposes an application-level event API for durable operations and delegates the realtime transport to Vix.
The retry behavior matters here. When the same durable request is replayed from storage, the handler does not run again, so the event is not emitted again by the handler. This avoids duplicate realtime notifications for the same completed operation.
Complete example with realtime
#include <vix.hpp>
#include <cnerium/cnerium.hpp>
#include <string>
int main()
{
vix::App app;
auto cnerium = cnerium::attach(app);
cnerium.config().enable_realtime("/ws", "0.0.0.0", 9090);
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true},
{"service", "orders"}
});
});
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}
});
});
if (!cnerium.start())
{
return 1;
}
app.run();
return 0;
}Use realtime only when the operation has something meaningful to announce. The durable response remains the source of truth. The event is a notification.
What this example proves
This example proves the core Cnerium behavior:
A normal Vix route remains a normal Vix route.
A critical POST route can be made durable with Cnerium.
A retry with the same key and body receives the stored response.
A retry with the same key and a different body is rejected.
The durable handler is not executed twice for the same completed request.
Realtime events are emitted only when the handler runs.The important lesson is not that Cnerium changes how Vix backends are built. It does not. The important lesson is that Cnerium gives selected Vix routes a stronger execution contract.
Next step
Continue with Cnerium and Vix to understand the boundary between the Vix application model and the Cnerium reliability layer.