Skip to main content

SochDB WebSocket Gateway

The sochdb-grpc-server binary can serve a JSON-over-WebSocket gateway in addition to its gRPC port. The gateway lets browsers and other thin clients talk to SochDB without gRPC tooling — every message is a small JSON envelope sent over a single WebSocket connection. It is implemented in sochdb-grpc/src/ws_server.rs on top of tokio-tungstenite.

For the gRPC surface, ports, and authentication model, see the gRPC Server page.

Enabling the gateway

The gateway is controlled by a single flag on the server binary:

FlagDefaultPurpose
--ws-port8080WebSocket gateway port. Set to 0 to disable.

The listener binds <host>:<ws-port> (the same --host used for gRPC) and accepts connections on the root path /. On startup the banner prints the connection URL:

# Start the server; the WebSocket gateway comes up on ws://127.0.0.1:8080/
sochdb-grpc-server

# Bind all interfaces and move the gateway to port 9001
sochdb-grpc-server --host 0.0.0.0 --ws-port 9001

# Disable the WebSocket gateway entirely
sochdb-grpc-server --ws-port 0

The connection endpoint is therefore ws://<host>:<ws-port>/, for example ws://127.0.0.1:8080/.

No authentication on the gateway

The WebSocket gateway is not wired through the gRPC auth interceptor. Unlike the gRPC services, it does not validate JWTs or API keys and does not apply RBAC. Keep --ws-port bound to loopback (the default --host 127.0.0.1) or place it behind an authenticating reverse proxy before exposing it on a network.

Message protocol

The protocol is symmetric: the client sends a WsRequest and the server replies with a WsResponse. Both share the same three-field envelope.

Request envelope (WsRequest)

{
"id": "req-1",
"type": "sql",
"payload": { "query": "SELECT 1" }
}
FieldTypeNotes
idstringClient-chosen correlation ID; echoed back on the response.
typestringOne of the client message types below.
payloadobjectType-specific payload. Defaults to an empty value if omitted.

Response envelope (WsResponse)

{
"id": "req-1",
"type": "result",
"payload": { "found": true, "value": "hello" }
}

The id is copied from the request so you can match replies to outstanding requests. The type is one of the server message types below.

Message types

Client typePurposeServer reply type
sqlExecute a SQL queryresult
kv_getRead a key-value entryresult
kv_putWrite a key-value entryresult
kv_deleteDelete a key-value entryresult
subscribeSubscribe to CDC events (streaming)event (pushed), or error
pingLiveness checkpong

Any unrecognized type, malformed JSON, or invalid payload produces an error response whose payload contains a message field, for example { "message": "Unknown message type: foo" }.

WebSocket ping vs. protocol ping

The application-level ping message type returns a pong envelope with a { "ts": <unix_ms> } payload. This is separate from the low-level WebSocket control-frame ping/pong, which the server also answers automatically.

Payload shapes

The payload object is deserialized into a typed struct per message type.

sqlSqlPayload

{
"query": "SELECT * FROM users",
"params": []
}
FieldTypeDefaultNotes
querystringrequiredSQL text.
paramsarray[]Positional parameters.

kv_getKvGetPayload

{ "key": "session-42", "namespace": "default" }
FieldTypeDefaultNotes
keystringrequiredLogical key.
namespacestring"default"Namespace prefix; the stored key is namespace:key.

kv_putKvPutPayload

{ "key": "session-42", "value": "hello", "namespace": "default", "ttl_seconds": 3600 }
FieldTypeDefaultNotes
keystringrequiredLogical key.
valuestringrequiredStored as UTF-8 bytes.
namespacestring"default"Namespace prefix.
ttl_secondsinteger00 means no expiry; otherwise expires after this many seconds.

kv_deleteKvDeletePayload

{ "key": "session-42", "namespace": "default" }
FieldTypeDefaultNotes
keystringrequiredLogical key.
namespacestring"default"Namespace prefix.

subscribeSubscribePayload

{ "tables": ["users"], "operations": ["insert", "update"], "start_sequence": 0 }
FieldTypeDefaultNotes
tablesarray of strings[]Optional table filter; empty means all tables.
operationsarray of strings[]Operation-type filter.
start_sequenceinteger00 resumes from the latest CDC sequence; a value greater than 0 resumes from that sequence.

Response payloads

The result payload differs per message type:

Request typeresult payloadExample
sqlplaceholder echo (see caveat){ "message": "...", "query": "SELECT 1", "note": "..." }
kv_get (hit){ "found": true, "value": "<utf8>" }
kv_get (miss/expired){ "found": false }
kv_put{ "ok": true }
kv_delete{ "deleted": <bool> }deleted is true only if the key existed
ping{ "ts": <unix_ms> } (type pong)

A successful subscribe does not return a result. Instead the server pushes event envelopes asynchronously as CDC records arrive. Each event payload has the shape:

{
"sequence": 17,
"timestamp_us": 1718200000000000,
"txn_id": 42,
"table": "users",
"operation": "Insert"
}

Backend behavior in the default binary

The gateway is wired to its own lightweight in-process backend, not the same storage engine the gRPC services use. Understanding this is important before you build on it.

KV is an isolated in-memory store

In the default sochdb-grpc-server binary, the WebSocket gateway is constructed with a fresh in-memory KvStore (a DashMap) created just for the gateway. It is shared across WebSocket connections but is separate from the gRPC KvService store and from any persistent database. Data written via kv_put lives only for the lifetime of the process and is not visible to gRPC clients. Keys are namespaced internally as namespace:key, and TTLs are evaluated lazily on read.

sql is a protocol placeholder

The sql handler does not execute against a persistent database in the WebSocket layer. It validates the payload and echoes the query back in a result envelope (with a note to "Wire to SqlBridge for full execution"). Use the gRPC server or the PostgreSQL wire protocol for real SQL execution.

subscribe is not wired to CDC in the default binary

The gateway accepts a cdc_log (Option<Arc<CdcLog>>) in its WsConfig, but the default binary passes cdc_log: None. With no CDC log attached, a subscribe request returns an error envelope with payload { "message": "CDC not enabled" }. The subscription plumbing exists — when a CdcLog is supplied the gateway starts a CdcSubscriber (honoring tables and start_sequence, table filtering enforced) and streams event envelopes — but it is not enabled in the shipped server. For change-data-capture today, use the gRPC SubscriptionService described on the gRPC Server page.

Client example

The example below opens a connection, runs a ping, writes and reads a key, and correlates replies by id.

const ws = new WebSocket("ws://127.0.0.1:8080/");

ws.onopen = () => {
// Liveness check
ws.send(JSON.stringify({ id: "ping-1", type: "ping", payload: {} }));

// Write a key (lives only in the gateway's in-memory store)
ws.send(JSON.stringify({
id: "put-1",
type: "kv_put",
payload: { key: "greeting", value: "hello", namespace: "default", ttl_seconds: 60 },
}));

// Read it back
ws.send(JSON.stringify({
id: "get-1",
type: "kv_get",
payload: { key: "greeting", namespace: "default" },
}));
};

ws.onmessage = (msg) => {
const res = JSON.parse(msg.data); // { id, type, payload }
switch (res.id) {
case "ping-1":
console.log("pong at", res.payload.ts);
break;
case "get-1":
console.log("found:", res.payload.found, "value:", res.payload.value);
break;
}
if (res.type === "error") {
console.error("error:", res.payload.message);
}
};
This gateway is JSON-only

The gateway processes text WebSocket frames containing JSON. Binary frames are ignored. For high-throughput vector ingestion and the full SochDB feature set, use the gRPC services instead — see the gRPC Server page.

  • gRPC Server — the primary network surface, ports, auth, and CDC SubscriptionService.
  • IPC Server — the local Unix-socket interface.