Add Cnerium to a Vix Backend
This guide shows how to add Cnerium to an existing Vix backend.
The important idea is that Cnerium does not replace your Vix application. You keep your vix::App, your routes, your middleware, your server lifecycle, and your normal Vix workflow. Cnerium attaches to that application and adds durable route behavior only where the backend needs retry-safe write operations.
If your backend already works with Vix, adding Cnerium should feel like adding a focused reliability layer, not migrating to another framework.
Starting point
Assume you already have a Vix backend like this:
#include <vix.hpp>
int main()
{
vix::App app;
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true}
});
});
app.post("/orders", [](vix::Request &req, vix::Response &res)
{
const auto body = req.json();
const std::string product_id = body.value("product_id", "");
const int quantity = body.value("quantity", 0);
if (product_id.empty())
{
res.status(400).json({
{"error", "Missing required field: product_id"}
});
return;
}
if (quantity <= 0)
{
res.status(400).json({
{"error", "Field quantity must be greater than zero"}
});
return;
}
res.status(201).json({
{"ok", true},
{"product_id", product_id},
{"quantity", quantity}
});
});
app.run();
return 0;
}This is a normal Vix backend. It has a health route and an order creation route.
The problem is not the Vix structure. The problem is that POST /orders creates state. If the client retries after a timeout, the same handler may run twice.
Cnerium is added only around that kind of operation.
Add the Cnerium header
Add the Cnerium umbrella header next to the Vix header:
#include <vix.hpp>
#include <cnerium/cnerium.hpp>Vix remains the main application API. Cnerium adds the reliability API.
Attach Cnerium to the Vix app
After creating the Vix app, attach Cnerium:
vix::App app;
auto cnerium = cnerium::attach(app);This line is the main integration point.
Cnerium keeps a reference to the Vix application and registers durable routes into it. The vix::App remains owned by your application code. Cnerium does not move it, replace it, or run it for you.
Keep normal routes in Vix
Routes that do not need retry-safe write behavior should remain normal Vix routes.
app.get("/health", [](vix::Request &req, vix::Response &res)
{
(void)req;
res.json({
{"ok", true}
});
});This is the correct place for health checks, status endpoints, read-only APIs, static files, and other routes that do not need durable execution.
Do not make every route durable. Cnerium is for selected critical operations.
Convert a critical POST route
A normal POST /orders route usually looks like this:
app.post("/orders", [](vix::Request &req, vix::Response &res)
{
// create order
});With Cnerium, convert it to a durable route:
cnerium.durable_post(
"/orders",
"orders.create",
[](cnerium::DurableRequest &request)
{
// create order
});The path remains the same:
/ordersThe new part is the operation name:
orders.createThe operation name is used by Cnerium’s reliability layer. It scopes the idempotency key and stored response for this route.
Use a stable, explicit operation name. In production, changing it changes the idempotency namespace for that route.
Return a DurableResponse
A durable handler does not write directly to vix::Response.
Instead, it returns a cnerium::DurableResponse:
return cnerium::created({
{"ok", true}
});Cnerium needs this response object because it must be able to store it and replay it later if the client retries the same request.
A normal Vix route writes a response once. A Cnerium durable route may need to return the same response again after a timeout, without re-running the handler.
Complete converted example
Here is a simple Vix backend with Cnerium attached and one durable route:
#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;
}The backend is still a Vix backend.
The health route is still a normal Vix route. The order route is registered through Cnerium because it represents a write operation that should be safe under retries.
Start order
The lifecycle should remain explicit:
if (!cnerium.start())
{
return 1;
}
app.run();cnerium.start() starts Cnerium resources such as the store and optional realtime support. app.run() starts the Vix HTTP application.
Do not invert the ownership model. Vix still runs the backend.
Test the durable route
Start the application, then send a request with an Idempotency-Key:
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 response:
{
"ok": true,
"order_id": "ord_order-123",
"product_id": "p1",
"quantity": 2
}Send the same request again:
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}'The response should be the same. Cnerium should replay the stored response instead of running the handler again.
Now reuse the same 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 result:
HTTP/1.1 409 ConflictThis confirms that Cnerium can distinguish a safe retry from unsafe key reuse.
Add explicit configuration
The default attachment is enough for small examples:
auto cnerium = cnerium::attach(app);For a real service, use an explicit configuration:
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");
auto cnerium = cnerium::attach(app, std::move(config));This configures the Cnerium layer, not the Vix application. Use Vix configuration for Vix server behavior. Use Cnerium configuration for Cnerium reliability resources.
Add realtime support
If the durable route should emit application events, enable realtime support:
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));Then emit an event inside the durable handler:
cnerium.emit(
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)},
{"product_id", cnerium::Json(product_id)},
{"quantity", cnerium::Json(quantity)}
})
);If a safe retry is replayed from storage, the handler does not run again, so the event is not emitted again by the handler.
Integrate with existing route modules
In a larger backend, route registration is often split across files.
A clean pattern is to pass both the Vix app and the attached Cnerium layer to the module that needs durable routes:
void register_order_routes(
vix::App &app,
cnerium::AttachedApp &cnerium)
{
app.get("/orders/{id}", get_order);
cnerium.durable_post(
"/orders",
"orders.create",
create_order);
}This keeps the boundary clear. Vix is used for normal routes. Cnerium is used for critical write routes.
Avoid hiding Cnerium behind global state. The explicit attachment makes the backend easier to understand and easier to test.
What not to change
When adding Cnerium to a Vix backend, do not rewrite the whole application.
You do not need to replace vix::App. You do not need to move normal routes into Cnerium. You do not need to create a second server. You do not need to duplicate middleware. You do not need to change the Vix build and run workflow.
The integration should stay small:
include Cnerium
attach Cnerium to vix::App
convert selected critical POST routes to durable routes
start Cnerium resources before app.run()That is the intended model.
When to convert a route
Convert a route when retrying it can produce duplicate or inconsistent state.
Good candidates:
POST /orders
POST /payments
POST /invoices
POST /users/register
POST /subscriptions
POST /workflows/startLeave normal Vix routes alone when they are read-only or do not need durable retry semantics:
GET /health
GET /status
GET /products
GET /orders/{id}Cnerium is useful because it is selective. It protects the dangerous part of the backend without changing the rest of the application.
Summary
Adding Cnerium to a Vix backend should be a small architectural change.
You keep vix::App. You attach Cnerium. You leave normal routes in Vix. You move selected critical POST routes to cnerium.durable_post. Then you start Cnerium resources before running the Vix app.
The backend remains a Vix backend, but its critical write operations gain durable, idempotent, retry-safe behavior.