Architecture
This page describes the internal architecture of Cnerium.
It is written for contributors and advanced users who want to understand how Cnerium is structured behind the public API. Application developers should usually start with the public model:
vix::App app;
auto cnerium = cnerium::attach(app);That line is the center of Cnerium’s architecture.
Vix owns the backend application. Cnerium attaches to it. Softadastra provides the durable storage foundation behind the reliability layer.
Cnerium is not a second backend runtime. It is not a replacement for Vix. It is a reliability layer for selected Vix routes.
High-level model
Cnerium has one main responsibility:
Make selected Vix backend operations durable, idempotent, and safe under retries.The architecture follows that responsibility.
Application code
-> vix::App
-> normal Vix routes
-> Cnerium attached layer
-> durable routes
-> idempotency
-> replay protection
-> stored responses
-> realtime application events
-> Cnerium store
-> Softadastra SDKEach layer has a clear role.
Vix owns the backend application model.
Cnerium owns the durable route execution model.
Softadastra SDK owns the durable storage foundation used by Cnerium.
Design boundary
The most important boundary is between Vix and Cnerium.
Vix owns:
HTTP server
routing
middleware
request parsing
response writing
application lifecycle
runtime executor
WebSocket transport
build workflow
development workflow
production workflowCnerium owns:
attach model
durable route registration
durable request wrapper
durable response type
idempotency checks
request body hashing
replay protection
stored responses
Cnerium store keys
application-level realtime eventsSoftadastra SDK owns:
durable local storage foundation
SDK client API
storage primitives used by the Cnerium storeThis separation prevents Cnerium from becoming another backend universe. A developer who knows Vix should see Cnerium as a focused reliability extension.
Public architecture
The public architecture should look like this:
#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}
});
});
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 is the intended shape.
The application starts with vix::App. Normal routes stay in Vix. Critical write routes are registered through Cnerium. Cnerium starts its own resources. Vix runs the HTTP application.
Main modules
Cnerium is organized around a small set of modules.
include/cnerium/app
include/cnerium/http
include/cnerium/reliability
include/cnerium/store
include/cnerium/realtime
include/cnerium/adapters
include/cnerium/supportThe source tree follows the same structure:
src/app
src/http
src/reliability
src/store
src/realtime
src/adapters
src/supportEach module should remain narrow.
Cnerium should not grow into a general web framework. Vix already owns that layer.
app module
The app module contains the attachment and runtime coordination layer.
Important files:
include/cnerium/app/AppConfig.hpp
include/cnerium/app/AppRuntime.hpp
include/cnerium/app/AttachedApp.hppDepending on the current repository state, older App.hpp APIs may still exist for compatibility or transition. The preferred architecture is the attached model:
vix::App app;
auto cnerium = cnerium::attach(app);The app module coordinates:
Cnerium configuration
runtime startup
store ownership
durable route registration
optional realtime startup
access to the attached vix::AppIt should not own the Vix application.
AttachedApp
AttachedApp is the public object returned by cnerium::attach.
It keeps a reference to vix::App and owns the Cnerium resources needed by durable routes.
Conceptually:
AttachedApp
references vix::App
owns AppRuntime
owns durable route registrations
exposes durable_post
exposes emit and emit_to
exposes start and stopThe object must stay alive while the Vix application is serving requests.
This is why examples store it in a variable:
auto cnerium = cnerium::attach(app);and avoid temporary attachment:
cnerium::attach(app).durable_post(...);AppRuntime
AppRuntime owns runtime resources used by Cnerium.
It is responsible for preparing and stopping internal services such as:
Cnerium store
Softadastra SDK-backed storage access
realtime adapter when enabled
runtime stateApplication code should rarely need to access AppRuntime directly. It exists behind AttachedApp.
The public lifecycle should remain:
if (!cnerium.start())
{
return 1;
}
app.run();cnerium.start() starts Cnerium resources. app.run() starts the Vix HTTP application.
AppConfig
AppConfig describes Cnerium runtime settings.
It includes values such as:
service name
data directory
node id
Vix config path
realtime endpoint
realtime host
realtime portExample:
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));This configuration belongs to Cnerium. It does not replace Vix configuration.
http module
The http module contains the durable route HTTP-facing model.
Important files:
include/cnerium/http/DurableRequest.hpp
include/cnerium/http/DurableResponse.hpp
include/cnerium/http/DurableHandler.hpp
include/cnerium/http/DurableRoute.hppThis module does not implement an HTTP server. Vix already does that.
The http module defines how a durable route sees a request, returns a response, and executes a reliability-protected handler.
DurableRequest
DurableRequest wraps the Vix HTTP request.
It gives durable handlers access to:
method
target
path
body
headers
Idempotency-Key
request hash
JSON body
route parameters
query parameters
native Vix requestIt exists because durable handlers need retry-specific helpers.
Normal Vix routes should use Vix request types. Durable Cnerium routes should use DurableRequest.
DurableResponse
DurableResponse is the response returned by a durable handler.
It must be storable because Cnerium may need to replay it later.
A durable response contains:
HTTP status code
response body
content typeThe route handler returns it:
return cnerium::created({
{"ok", true},
{"order_id", order_id}
});Cnerium converts it into a Vix response for the current request and into a stored response for future safe retries.
DurableRoute
DurableRoute is the execution object behind cnerium.durable_post.
It owns:
operation name
store reference
user durable handlerIts job is to execute this flow:
receive Vix request
wrap it as DurableRequest
read Idempotency-Key
compute request body hash
check replay protection
execute handler only for new safe requests
store response
replay stored response for safe retries
reject unsafe key reuse
reject missing keysApplication code usually does not instantiate DurableRoute directly. AttachedApp::durable_post creates and stores it.
reliability module
The reliability module contains the durable decision logic.
Important files:
include/cnerium/reliability/Idempotency.hpp
include/cnerium/reliability/IdempotencyKey.hpp
include/cnerium/reliability/ReplayProtection.hpp
include/cnerium/reliability/RequestHash.hpp
include/cnerium/reliability/DurableResult.hppThis module decides whether a durable request should:
execute
replay
conflict
fail as invalidIt does not know about the Vix server. It works with operation names, keys, hashes, and stored metadata.
IdempotencyKey
IdempotencyKey represents the Idempotency-Key header.
It is a small value object around the key string.
A valid key identifies one logical client operation attempt. The same key should be reused only when retrying the same request body.
RequestHash
RequestHash represents a stable hash of the request body.
Cnerium uses this hash to detect unsafe key reuse.
Safe retry:
same operation
same key
same request body hashUnsafe reuse:
same operation
same key
different request body hashThe hash must be stable. It should not rely on implementation-defined std::hash behavior.
ReplayProtection
ReplayProtection checks stored metadata and returns a durable decision.
Its rules are:
missing or invalid key
-> Invalid
new key
-> Execute
same key with same body hash
-> Replay
same key with different body hash
-> ConflictThis logic is the heart of Cnerium’s reliability model.
Idempotency
Idempotency coordinates request body hashing and replay protection.
It exposes methods such as:
check
check_hash
commit
commit_hash
hash_bodyDurableRoute normally calls this service internally. Application code should usually use cnerium.durable_post instead of calling Idempotency directly.
DurableResult
DurableResult carries the result of a replay protection check.
It can represent:
Execute
Replay
Conflict
InvalidFor replay results, it also carries the stored response that should be returned.
DurableRoute converts this result into HTTP behavior.
store module
The store module stores Cnerium reliability metadata.
Important files:
include/cnerium/store/Store.hpp
include/cnerium/store/StoreKey.hpp
include/cnerium/store/StoredResponse.hppThe store is not the application database.
It stores:
request hashes
stored responses
operation metadata
framework runtime metadataApplication domain data remains the responsibility of the application.
StoredResponse
StoredResponse is the persisted form of a durable response.
It contains:
status code
body
content typeWhen a safe retry arrives, Cnerium loads the stored response and writes it back through the Vix response writer.
StoreKey
StoreKey is used to build consistent internal keys for Cnerium metadata.
Conceptually, Cnerium stores data under keys such as:
cnerium:hash:<operation>:<key>
cnerium:response:<operation>:<key>Application code should not depend on the exact key format. It is an internal detail.
Store
Store is the facade used by the reliability layer.
It sits between Cnerium’s idempotency logic and the Softadastra SDK-backed storage foundation.
The dependency direction is:
DurableRoute
-> Idempotency
-> ReplayProtection
-> Store
-> Softadastra SDKThis keeps Softadastra details behind a small Cnerium storage API.
realtime module
The realtime module contains the application-level event model.
Important files:
include/cnerium/realtime/Event.hpp
include/cnerium/realtime/EventPayload.hpp
include/cnerium/realtime/Realtime.hpp
include/cnerium/realtime/RealtimeConfig.hppCnerium realtime does not implement the WebSocket transport.
It defines:
event type
event payload
realtime configuration
emit behaviorVix handles the WebSocket server and transport.
Event
Event represents an application event.
Example:
cnerium::Event event{
"order.created",
cnerium::support::object({
{"order_id", cnerium::Json(order_id)}
})};Event names should describe completed facts:
order.created
payment.created
invoice.created
user.registered
workflow.startedEventPayload
EventPayload is a JSON payload used by realtime events.
It is backed by Cnerium JSON, which is backed by Vix JSON.
The payload should contain useful identifiers and minimal state needed by connected clients.
RealtimeConfig
RealtimeConfig stores realtime settings:
enabled
endpoint
host
portIt is usually configured through AppConfig:
config.enable_realtime("/ws", "0.0.0.0", 9090);adapters module
The adapters module connects Cnerium to Vix and Softadastra.
Important files:
include/cnerium/adapters/VixHttp.hpp
include/cnerium/adapters/VixWebSocket.hpp
include/cnerium/adapters/SoftadastraStore.hppAdapters must remain thin.
They should translate between Cnerium types and external ecosystem types without duplicating the external system.
VixHttp adapter
VixHttp bridges Cnerium durable responses and Vix HTTP responses.
It is responsible for:
executing a DurableRoute from a Vix request
writing DurableResponse into Vix response wrapper
preserving status code
preserving body
preserving content typeIt does not implement an HTTP server, router, middleware layer, or request parser.
Vix already owns those responsibilities.
VixWebSocket adapter
VixWebSocket bridges Cnerium realtime events to the Vix WebSocket runtime.
It is responsible for:
starting Vix WebSocket support when configured
stopping realtime support
converting Cnerium event payloads to Vix JSON payloads
emitting events
emitting events to roomsIt does not implement a second WebSocket protocol or session system.
SoftadastraStore adapter
SoftadastraStore bridges Cnerium store operations to the Softadastra SDK.
It is responsible for storage integration, not application domain logic.
Cnerium should depend on the public Softadastra SDK, not on internal Softadastra engine APIs.
This matters for the public direction of the project. Cnerium should expose a clean backend reliability API. The Softadastra SDK should remain the durable foundation behind that API.
support module
The support module contains small shared utility types and helpers.
Important files:
include/cnerium/support/Error.hpp
include/cnerium/support/Json.hpp
include/cnerium/support/Result.hpp
include/cnerium/support/String.hppThis module should stay small.
It should not become a general-purpose framework utility library. If a feature belongs to Vix, it should remain in Vix. If it belongs to Softadastra, it should remain in the Softadastra SDK.
Json
Cnerium uses the Vix JSON type.
This keeps Cnerium aligned with the Vix backend and WebSocket model.
using Json = vix::json::Json;Public helpers such as support::object, support::string_or, and support::int_or make examples easier to read.
Result
Cnerium can reuse the Softadastra core result type where appropriate.
The purpose is to avoid duplicating common primitives already provided by the ecosystem.
Cnerium should reuse stable ecosystem foundations instead of creating parallel versions.
Request lifecycle
A durable request follows this internal path:
client
-> Vix HTTP server
-> Vix router
-> Vix route callback registered by Cnerium
-> VixHttp adapter
-> DurableRoute
-> DurableRequest
-> Idempotency
-> ReplayProtection
-> Store
-> DurableHandler, only if request should execute
-> DurableResponse
-> StoredResponse commit
-> Vix response writerThe important point is that Vix remains the HTTP owner from beginning to end. Cnerium only controls the durable operation decision.
Normal route lifecycle
A normal Vix route remains simple:
client
-> Vix HTTP server
-> Vix router
-> Vix handler
-> Vix response writerCnerium should not interfere with normal routes.
A backend can mix both:
app.get("/health", health_handler);
app.get("/orders/{id}", get_order);
cnerium.durable_post("/orders", "orders.create", create_order);Durable route lifecycle
A durable route adds a reliability decision before handler execution:
client
-> Vix HTTP server
-> Vix router
-> Cnerium DurableRoute
-> missing key
-> 400 Bad Request
-> new key
-> execute handler
-> store response
-> return response
-> same key and same body
-> replay stored response
-> same key and different body
-> 409 Conflict
-> Vix response writerThe handler only runs in the new request case.
Realtime lifecycle
When a durable handler emits an event:
DurableHandler
-> cnerium.emit
-> Realtime
-> VixWebSocket adapter
-> Vix WebSocket runtime
-> connected clientsIf a safe retry is replayed from storage, the handler does not run, so the event is not emitted again by that handler.
This is intentional. Realtime events should follow successful operation execution, not HTTP retry count.
Startup lifecycle
A typical application startup is:
create vix::App
create Cnerium AppConfig
attach Cnerium to vix::App
register normal Vix routes
register durable Cnerium routes
start Cnerium resources
run Vix appIn code:
vix::App app;
auto cnerium = cnerium::attach(app);
register_routes(app, cnerium);
if (!cnerium.start())
{
return 1;
}
app.run();Cnerium starts before the Vix server begins serving durable routes.
Shutdown lifecycle
Cnerium should stop only the resources it owns.
It should not destroy the Vix application because it does not own it.
A clean shutdown should respect this boundary:
Vix application stops serving
Cnerium runtime resources stop
Cnerium store flushes or closes
realtime adapter stops
objects are destroyed in normal C++ orderIn simple applications, object lifetime in main handles most of this naturally.
Dependency direction
Cnerium should keep a strict dependency direction.
Cnerium public API
-> Cnerium internals
-> Vix public API
-> Softadastra SDK public APICnerium should not depend on private Vix internals or private Softadastra engine internals.
The public promise is:
Vix is the runtime and backend foundation.
Cnerium is the reliability layer.
Softadastra SDK is the durable storage foundation.What should not be added to Cnerium
Cnerium should not become the place for everything.
Avoid adding:
a second HTTP server
a second router
a second middleware system
a second WebSocket stack
a database ORM
a template engine
a full authentication framework
a frontend framework
a deployment system
a replacement for Vix CLIThose belong elsewhere in the ecosystem.
Cnerium should remain focused on backend reliability:
durable routes
idempotency
stored responses
replay protection
retry-safe handlers
realtime notifications tied to durable operations
Softadastra-backed reliability storageWhy this architecture matters
The architecture exists to avoid confusion.
If Cnerium exposes a complete separate application model, a Vix developer will reasonably ask why they should leave Vix to build a backend. That is the wrong message.
Cnerium should make the answer obvious:
You do not leave Vix.
You keep Vix.
You attach Cnerium only where retry safety matters.That is the architectural identity of Cnerium.
Summary
Cnerium is a reliability layer attached to Vix.
The architecture is built around that boundary. Vix owns the backend application. Cnerium owns durable route behavior. Softadastra SDK provides the durable storage foundation behind Cnerium.
Internally, Cnerium is split into app, http, reliability, store, realtime, adapters, and support modules. Each module has a narrow responsibility. Together, they make selected Vix routes durable, idempotent, replay-safe, and easier to reason about under retries and unstable network conditions.