Message store
This page explains the message persistence abstraction in the Vix WebSocket module.
Use it when you want to store WebSocket messages, list room history, replay messages after reconnect, or plug your own durable backend into the realtime layer.
Header
#include <vix/websocket/MessageStore.hpp>Or use the umbrella header:
#include <vix/websocket.hpp>What the message store provides
The WebSocket module defines an abstract message storage interface:
vix::websocket::IMessageStoreIt is used to persist JsonMessage values.
A message store can support:
- chat history
- room history
- missed message replay
- event replay
- durable realtime logs
- local-first message persistence
- custom storage backends
The module also provides a SQLite implementation:
vix::websocket::SqliteMessageStoreBasic model
The model is:
WebSocket message
-> JsonMessage
-> IMessageStore::append(...)
-> durable storage2
3
4
Then clients can read history:
room id
-> IMessageStore::list_by_room(...)
-> previous messages2
3
Or replay messages:
start id
-> IMessageStore::replay_from(...)
-> messages after cursor2
3
IMessageStore
IMessageStore is the abstract persistence interface.
class IMessageStore
{
public:
virtual ~IMessageStore() = default;
virtual void append(const JsonMessage &msg) = 0;
virtual std::vector<JsonMessage> list_by_room(
const std::string &room,
std::size_t limit,
const std::optional<std::string> &before_id = std::nullopt) = 0;
virtual std::vector<JsonMessage> replay_from(
const std::string &start_id,
std::size_t limit) = 0;
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
It stores and returns vix::websocket::JsonMessage.
JsonMessage
The store works with the WebSocket typed message model.
A message can contain:
id
kind
room
type
ts
payload2
3
4
5
6
Example:
{
"id": "00000000000000000001",
"kind": "event",
"room": "general",
"type": "chat.message",
"ts": "2026-05-17T10:00:00Z",
"payload": {
"text": "Hello"
}
}2
3
4
5
6
7
8
9
10
append
Use append(...) to persist a message.
store.append(message);Example:
vix::websocket::JsonMessage message;
message.kind = "event";
message.room = "general";
message.type = "chat.message";
message.payload = {
{"text", "Hello"}
};
store.append(message);2
3
4
5
6
7
8
9
Use append(...) when:
- a chat message is accepted
- an event must be durable
- a room update should be replayable
- clients may reconnect and need missed messages
list_by_room
Use list_by_room(...) to load messages for one room.
auto messages = store.list_by_room("general", 50);Arguments:
| Argument | Purpose |
|---|---|
room | Room identifier. |
limit | Maximum number of messages to return. |
before_id | Optional cursor for pagination. |
Example with cursor:
auto messages =
store.list_by_room("general", 50, "00000000000000000020");2
Use list_by_room(...) for:
- chat history
- room timeline
- previous events
- paginated message history
- loading old messages when a client joins
replay_from
Use replay_from(...) to replay messages starting from a cursor.
auto messages = store.replay_from("00000000000000000001", 100);Arguments:
| Argument | Purpose |
|---|---|
start_id | Cursor id to start replay from. |
limit | Maximum number of messages to return. |
Use replay_from(...) for:
- reconnect recovery
- missed events
- event replay
- local-first synchronization
- durable realtime streams
Basic usage
#include <vix/websocket.hpp>
int main()
{
vix::websocket::SqliteMessageStore store{"messages.db"};
vix::websocket::JsonMessage message;
message.kind = "event";
message.room = "general";
message.type = "chat.message";
message.payload = {
{"text", "Hello from Vix"}
};
store.append(message);
auto history = store.list_by_room("general", 50);
(void)history;
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Store messages from WebSocket handlers
A common pattern is to store typed messages before broadcasting them.
vix::websocket::SqliteMessageStore store{"messages.db"};
ws.on_typed_message(
[&ws, &store](vix::websocket::Session &session,
const std::string &type,
const vix::json::kvs &payload)
{
(void)session;
if (type != "chat.message")
{
return;
}
vix::websocket::JsonMessage message;
message.kind = "event";
message.room = "general";
message.type = type;
message.payload = payload;
store.append(message);
ws.broadcast_text_to_room("general", "new chat message");
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
The flow is:
client sends typed message
-> server validates type
-> message store appends JsonMessage
-> server broadcasts to room2
3
4
Store before broadcast
For durable realtime systems, prefer storing before broadcasting.
receive message
-> validate
-> append to store
-> broadcast2
3
4
This makes the message durable before clients are notified.
That is useful when clients may reconnect and ask for missed messages.
Room history
Messages with a room field can be queried by room.
vix::websocket::JsonMessage message;
message.room = "chat:general";
message.type = "chat.message";
message.payload = {
{"text", "Hello"}
};
store.append(message);2
3
4
5
6
7
8
Then load the room history:
auto messages = store.list_by_room("chat:general", 50);This is useful for:
- chat rooms
- project timelines
- tenant events
- user channels
- dashboards
Reconnect replay
When a client reconnects, it can ask for messages after the last message id it received.
Conceptually:
client reconnects
-> sends last_seen_id
-> server calls replay_from(last_seen_id, limit)
-> server sends missed messages2
3
4
Example:
auto missed = store.replay_from(lastSeenId, 100);
for (const auto &message : missed)
{
session.send_text(message.to_json_string());
}2
3
4
5
6
Custom message store
You can implement your own message store by inheriting from IMessageStore.
class MyMessageStore final : public vix::websocket::IMessageStore
{
public:
void append(const vix::websocket::JsonMessage &msg) override
{
// Store message in your backend.
}
std::vector<vix::websocket::JsonMessage> list_by_room(
const std::string &room,
std::size_t limit,
const std::optional<std::string> &before_id = std::nullopt) override
{
// Query room history from your backend.
return {};
}
std::vector<vix::websocket::JsonMessage> replay_from(
const std::string &start_id,
std::size_t limit) override
{
// Replay messages from your backend.
return {};
}
};2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Use a custom store when you want to persist messages in:
- PostgreSQL
- MySQL
- Redis Streams
- files
- object storage
- distributed logs
- custom local-first storage
SQLite implementation
The built-in implementation is:
vix::websocket::SqliteMessageStoreIt stores messages in SQLite and enables WAL mode.
vix::websocket::SqliteMessageStore store{"messages.db"};The SQLite table stores fields such as:
id
kind
room
type
ts
payload_json2
3
4
5
6
Use SQLite when you want:
- a simple durable local store
- chat history
- local development
- embedded message persistence
- WAL-friendly storage
Message id
A message id is used for cursors, replay, and pagination.
If your store generates ids automatically, the application can leave id empty.
If your application needs deterministic ids, set message.id before appending.
message.id = "00000000000000000001";Use stable ids when:
- clients track last seen messages
- replay must be deterministic
- events are synchronized across nodes
- messages are deduplicated
Timestamp
A message can carry a timestamp in ts.
message.ts = "2026-05-17T10:00:00Z";The SQLite store can fill missing timestamps automatically.
Use timestamps for:
- chat history display
- ordering
- audit logs
- replay diagnostics
- debugging
Message kind
The kind field lets you categorize messages.
Common value:
eventOther possible values:
command
system
notification
presence2
3
4
Keep kind simple and consistent.
Message type
The type field identifies the application event.
Examples:
chat.message
room.join
room.leave
presence.update
notification.created
order.updated2
3
4
5
6
Use clear domain.action names.
Payload
The payload contains message data.
Example:
message.payload = {
{"text", "Hello"},
{"user_id", "42"}
};2
3
4
Keep payloads small.
For large files, store the file elsewhere and send a reference.
Message store with rooms
A good room message shape is:
{
"kind": "event",
"room": "chat:general",
"type": "chat.message",
"payload": {
"text": "Hello"
}
}2
3
4
5
6
7
8
This gives you:
room history
typed routing
future replay
long-polling compatibility2
3
4
Message store with long-polling
The same JsonMessage can be used by the long-polling bridge.
Flow:
WebSocket typed message
-> append to message store
-> LongPollingBridge buffers message
-> HTTP clients poll message2
3
4
This gives both persistence and fallback delivery.
Message store with broadcasting
A durable broadcast flow can look like this:
receive typed message
-> build JsonMessage
-> store.append(message)
-> ws.broadcast_text_to_room(message.room, message.to_json_string())2
3
4
This keeps the broadcast event replayable later.
Error handling
Message stores can throw exceptions when storage fails.
Handle errors around persistence.
try
{
store.append(message);
}
catch (const std::exception &e)
{
session.emit_error(e.what());
session.close("message persistence failed");
}2
3
4
5
6
7
8
9
For production systems, decide whether a failed store should:
- reject the message
- close the session
- retry later
- fall back to memory
- return an error message to the client
Memory vs durable storage
Without a message store, realtime delivery is transient.
message sent
-> broadcast
-> gone after delivery2
3
With a message store:
message sent
-> append to store
-> broadcast
-> available for history and replay2
3
4
Use durable storage when messages matter after the first delivery.
Best practices
Store accepted messages before broadcasting.
Use typed messages for stored events.
Set a room when messages belong to a channel.
Use stable event type names.
Keep payloads small.
Use message ids for replay and pagination.
Use list_by_room(...) for room history.
Use replay_from(...) for reconnect recovery.
Handle persistence errors explicitly.
Common mistakes
Broadcasting before storing
Avoid this when durability matters:
broadcast
-> append to store2
Prefer:
append to store
-> broadcast2
Storing huge payloads
Avoid storing large blobs in message payloads.
Store files separately and keep a reference in the payload.
Missing room for room history
If the message should appear in a room history, set:
message.room = "chat:general";No replay cursor
Reconnect recovery needs a cursor.
Track the last message id received by the client.
Treating persistence as authorization
The message store only stores messages.
It does not decide who is allowed to read them.
Validate access before returning room history or replay data.
Next steps
Continue with: