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:
| Flag | Default | Purpose |
|---|---|---|
--ws-port | 8080 | WebSocket 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/.
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" }
}
| Field | Type | Notes |
|---|---|---|
id | string | Client-chosen correlation ID; echoed back on the response. |
type | string | One of the client message types below. |
payload | object | Type-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 type | Purpose | Server reply type |
|---|---|---|
sql | Execute a SQL query | result |
kv_get | Read a key-value entry | result |
kv_put | Write a key-value entry | result |
kv_delete | Delete a key-value entry | result |
subscribe | Subscribe to CDC events (streaming) | event (pushed), or error |
ping | Liveness check | pong |
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" }.
ping vs. protocol pingThe 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.
sql — SqlPayload
{
"query": "SELECT * FROM users",
"params": []
}
| Field | Type | Default | Notes |
|---|---|---|---|
query | string | required | SQL text. |
params | array | [] | Positional parameters. |
kv_get — KvGetPayload
{ "key": "session-42", "namespace": "default" }
| Field | Type | Default | Notes |
|---|---|---|---|
key | string | required | Logical key. |
namespace | string | "default" | Namespace prefix; the stored key is namespace:key. |
kv_put — KvPutPayload
{ "key": "session-42", "value": "hello", "namespace": "default", "ttl_seconds": 3600 }
| Field | Type | Default | Notes |
|---|---|---|---|
key | string | required | Logical key. |
value | string | required | Stored as UTF-8 bytes. |
namespace | string | "default" | Namespace prefix. |
ttl_seconds | integer | 0 | 0 means no expiry; otherwise expires after this many seconds. |
kv_delete — KvDeletePayload
{ "key": "session-42", "namespace": "default" }
| Field | Type | Default | Notes |
|---|---|---|---|
key | string | required | Logical key. |
namespace | string | "default" | Namespace prefix. |
subscribe — SubscribePayload
{ "tables": ["users"], "operations": ["insert", "update"], "start_sequence": 0 }
| Field | Type | Default | Notes |
|---|---|---|---|
tables | array of strings | [] | Optional table filter; empty means all tables. |
operations | array of strings | [] | Operation-type filter. |
start_sequence | integer | 0 | 0 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 type | result payload | Example |
|---|---|---|
sql | placeholder 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.
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 placeholderThe 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 binaryThe 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.
- Browser / Node
- Python (websockets)
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);
}
};
import asyncio
import json
import websockets # pip install websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8080/") as ws:
# Liveness check
await ws.send(json.dumps({"id": "ping-1", "type": "ping", "payload": {}}))
# Write then read a key (gateway-local in-memory store)
await ws.send(json.dumps({
"id": "put-1",
"type": "kv_put",
"payload": {"key": "greeting", "value": "hello", "ttl_seconds": 60},
}))
await ws.send(json.dumps({
"id": "get-1",
"type": "kv_get",
"payload": {"key": "greeting"},
}))
for _ in range(3):
res = json.loads(await ws.recv()) # {"id", "type", "payload"}
print(res["id"], res["type"], res["payload"])
asyncio.run(main())
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.
Related pages
- gRPC Server — the primary network surface, ports, auth, and CDC
SubscriptionService. - IPC Server — the local Unix-socket interface.