Vix.cpp v2.8.5 is here Read the blog
Skip to content

SQLite message store ​

This page explains vix::websocket::SqliteMessageStore.

Use it when you want to persist WebSocket JsonMessage values in SQLite, keep room history, replay messages, or build a durable local message backend.

cpp
#include <vix/websocket/SqliteMessageStore.hpp>

Or use the umbrella header:

cpp
#include <vix/websocket.hpp>

What SqliteMessageStore provides ​

SqliteMessageStore is the built-in SQLite implementation of:

cpp
vix::websocket::IMessageStore

It stores WebSocket messages in a SQLite database.

It supports:

  • appending messages
  • listing messages by room
  • replaying messages from a cursor
  • automatic schema initialization
  • automatic id generation when missing
  • automatic timestamp generation when missing
  • SQLite WAL mode
  • durable local message history

Basic model ​

The model is:

txt
JsonMessage
  -> SqliteMessageStore::append(...)
  -> SQLite database
  -> messages table

Then messages can be queried:

txt
room
  -> list_by_room(...)
  -> room history

Or replayed:

txt
start_id
  -> replay_from(...)
  -> message replay

Create a store ​

cpp
vix::websocket::SqliteMessageStore store{"messages.db"};

If the database does not exist, SQLite creates it.

The store initializes the message schema automatically.

Basic usage ​

cpp
#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;
}

Constructor ​

cpp
explicit SqliteMessageStore(const std::string &db_path);

The constructor opens or creates the SQLite database.

cpp
vix::websocket::SqliteMessageStore store{"messages.db"};

During construction, the store:

txt
1. opens the SQLite database
2. enables SQLite WAL mode
3. initializes the messages table

The implementation opens SQLite with sqlite3_open(...), enables PRAGMA journal_mode=WAL, then calls init_schema(). :contentReference[oaicite:0]

Destructor ​

cpp
~SqliteMessageStore() override;

The destructor closes the SQLite database handle.

txt
~SqliteMessageStore
  -> sqlite3_close
  -> db_ = nullptr

Schema ​

The store creates a table named:

txt
messages

The schema contains:

txt
id
kind
room
type
ts
payload_json

Conceptually:

sql
CREATE TABLE IF NOT EXISTS messages (
  id           TEXT PRIMARY KEY,
  kind         TEXT NOT NULL,
  room         TEXT,
  type         TEXT NOT NULL,
  ts           TEXT NOT NULL,
  payload_json TEXT NOT NULL
);

The table is created automatically if it does not exist. :contentReference[oaicite:1]

Stored fields ​

FieldPurpose
idUnique message id and replay cursor.
kindMessage category, usually event.
roomOptional room or channel id.
typeApplication event type.
tsTimestamp.
payload_jsonSerialized JSON payload.

append ​

Use append(...) to persist a message.

cpp
store.append(message);

Signature:

cpp
void append(const JsonMessage &msg) override;

Example:

cpp
vix::websocket::JsonMessage message;
message.kind = "event";
message.room = "chat:general";
message.type = "chat.message";
message.payload = {
  {"text", "Hello"}
};

store.append(message);

Automatic id ​

If message.id is empty, the SQLite store generates an id.

cpp
vix::websocket::JsonMessage message;
message.id = "";
message.type = "chat.message";

store.append(message);

The current implementation generates an id based on the current timestamp in microseconds and formats it as a padded string. :contentReference[oaicite:2]

Automatic timestamp ​

If message.ts is empty, the SQLite store generates a UTC timestamp.

cpp
vix::websocket::JsonMessage message;
message.ts = "";
message.type = "chat.message";

store.append(message);

The timestamp shape is:

txt
YYYY-MM-DDTHH:MM:SSZ

The implementation fills ts before inserting when the field is missing. :contentReference[oaicite:3]

Default kind ​

If message.kind is empty, the store uses:

txt
event

Example:

cpp
vix::websocket::JsonMessage message;
message.kind = "";
message.type = "chat.message";

store.append(message);

After normalization, the message kind becomes:

txt
event

Payload serialization ​

The store serializes message.payload to JSON before insertion.

txt
vix::json::kvs
  -> nlohmann::json
  -> payload_json text

This allows structured payloads to be stored in one SQLite text column.

Insert behavior ​

The SQLite store uses insert-or-replace behavior.

Conceptually:

sql
INSERT OR REPLACE INTO messages
(id, kind, room, type, ts, payload_json)
VALUES (?, ?, ?, ?, ?, ?);

This means a message with an existing id can replace the previous row.

list_by_room ​

Use list_by_room(...) to load messages for one room.

cpp
auto messages = store.list_by_room("chat:general", 50);

Signature:

cpp
std::vector<JsonMessage> list_by_room(
    const std::string &room,
    std::size_t limit,
    const std::optional<std::string> &before_id = std::nullopt) override;

Arguments:

ArgumentPurpose
roomRoom identifier.
limitMaximum number of messages to return.
before_idOptional pagination cursor.

List room history ​

cpp
auto history = store.list_by_room("general", 50);

for (const auto &message : history)
{
  // send or inspect message
}

Use this when:

  • a user opens a chat room
  • a dashboard loads previous events
  • a client reconnects
  • a room timeline must be restored

Pagination with before_id ​

Use before_id to load messages before a cursor.

cpp
auto older = store.list_by_room(
    "chat:general",
    50,
    "00000000000000001000");

This is useful for paginated history.

Example flow:

txt
load latest 50 messages
  -> user scrolls up
  -> request older messages before oldest id
  -> list_by_room(room, 50, before_id)

replay_from ​

Use replay_from(...) to replay messages from a cursor.

cpp
auto messages = store.replay_from("00000000000000000001", 100);

Signature:

cpp
std::vector<JsonMessage> replay_from(
    const std::string &start_id,
    std::size_t limit) override;

Arguments:

ArgumentPurpose
start_idCursor id to start from.
limitMaximum number of messages to return.

Replay after reconnect ​

A reconnect flow can look like this:

txt
client reconnects
  -> client sends last_seen_id
  -> server calls replay_from(last_seen_id, limit)
  -> server sends missed messages

Example:

cpp
auto missed = store.replay_from(lastSeenId, 100);

for (const auto &message : missed)
{
  session.send_text(message.to_json_string());
}

Store before broadcast ​

For durable realtime behavior, store messages before broadcasting them.

cpp
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 = "chat:general";
    message.type = type;
    message.payload = payload;

    store.append(message);

    ws.broadcast_text_to_room(
        "chat:general",
        message.to_json_string());
  });

The flow is:

txt
receive typed message
  -> validate
  -> append to SQLite
  -> broadcast to room

This makes the event durable before clients are notified.

Chat room example ​

cpp
vix::websocket::SqliteMessageStore store{"chat.db"};

ws.on_open([&ws](vix::websocket::Session &session)
{
  ws.join_room(session.shared_from_this(), "chat:general");

  auto history = store.list_by_room("chat:general", 50);

  for (const auto &message : history)
  {
    session.send_text(message.to_json_string());
  }
});

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 = "chat:general";
    message.type = type;
    message.payload = payload;

    store.append(message);

    ws.broadcast_text_to_room(
        "chat:general",
        message.to_json_string());
  });

Database file location ​

You can store the SQLite database wherever your application has write access.

cpp
vix::websocket::SqliteMessageStore store{"data/messages.db"};

For local development:

txt
messages.db
chat.db
.vix/messages.db

For production:

txt
/var/lib/myapp/messages.db
/opt/myapp/data/messages.db

Make sure the application user can write to the directory.

WAL mode ​

The SQLite store enables WAL mode.

sql
PRAGMA journal_mode=WAL;

WAL mode is useful because it improves durability and supports better concurrent read behavior.

This fits WebSocket applications where writes and reads can happen frequently.

Error handling ​

SQLite operations can throw exceptions when they fail.

Handle persistence errors in your application.

cpp
try
{
  store.append(message);
}
catch (const std::exception &e)
{
  session.emit_error(e.what());
  session.close("message persistence failed");
}

Use this when persistence is required for correctness.

Required fields ​

Before storing, make sure a message has at least:

txt
type
payload

The store can fill:

txt
id
ts
kind

But the application should set meaningful values for:

txt
room
type
payload

when it wants useful history and replay.

Room field ​

Set room when messages belong to a room.

cpp
message.room = "chat:general";

Without a room, list_by_room(...) cannot return the message for a room history.

Type field ​

Set type to describe the application event.

cpp
message.type = "chat.message";

Use clear event names:

txt
chat.message
notification.created
presence.update
order.updated

Payload field ​

Set payload to carry event data.

cpp
message.payload = {
  {"text", "Hello"},
  {"user_id", "42"}
};

Keep payloads small.

For files, store a file URL or id instead of embedding large data.

Id field ​

Set id manually only if your application needs deterministic ids.

cpp
message.id = "00000000000000000042";

Otherwise, let the store generate it.

Timestamp field ​

Set ts manually only if your application already has a trusted timestamp.

cpp
message.ts = "2026-05-17T10:00:00Z";

Otherwise, let the store generate it.

Kind field ​

Use kind to classify messages.

Common default:

txt
event

Examples:

txt
event
command
system
notification
presence

Keep this field stable across your application.

SQLite store vs custom store ​

Use SqliteMessageStore when:

  • you need a simple local durable store
  • the application is small or medium-sized
  • you want embedded persistence
  • you want local development history
  • you want WAL-friendly message storage

Use a custom IMessageStore implementation when:

  • you need PostgreSQL
  • you need MySQL
  • you need Redis Streams
  • you need distributed storage
  • you need cloud storage
  • you need application-specific indexing

Performance notes ​

SQLite is a good default for local durable message history.

Keep these rules in mind:

  • keep payloads small
  • use room ids consistently
  • use limits for queries
  • avoid loading unlimited history
  • use pagination
  • avoid storing large binary blobs
  • store references to files instead of file content

Production notes ​

For production use:

  • store the database in a persistent directory
  • back up the database
  • monitor disk usage
  • keep message limits reasonable
  • avoid unbounded history growth
  • consider retention policies
  • protect history endpoints with authorization

Retention strategy ​

The current interface focuses on append, room listing, and replay.

If your application needs retention, add a cleanup policy around the store.

Examples:

txt
delete messages older than 30 days
keep last 10,000 messages per room
archive old messages to object storage
compact system events periodically

Security notes ​

Message storage does not provide authorization.

Before returning room history or replayed messages, validate that the client is allowed to read them.

txt
client asks for room history
  -> authenticate client
  -> authorize room access
  -> list_by_room
  -> send messages

Do not expose stored message history without access checks.

Best practices ​

Store messages before broadcasting when durability matters.

Set room for room history.

Set clear type values.

Keep payloads small.

Use limit for all history queries.

Use before_id for pagination.

Use replay_from(...) for reconnect recovery.

Handle SQLite exceptions.

Store the database in a persistent path.

Protect history and replay endpoints with authorization.

Common mistakes ​

Missing room ​

Avoid:

cpp
message.type = "chat.message";
store.append(message);

For room history, set:

cpp
message.room = "chat:general";

Loading unlimited history ​

Avoid APIs that return all messages.

Prefer:

cpp
store.list_by_room("chat:general", 50);

Broadcasting before append ​

Avoid:

txt
broadcast
  -> append

Prefer:

txt
append
  -> broadcast

Storing large files in payload_json ​

Do not store large binary payloads in payload_json.

Store a file reference instead.

Ignoring storage errors ​

Do not silently ignore append failures when durability matters.

Handle exceptions.

Next steps ​

Continue with:

Released under the MIT License.