Response Format
Every request over the WebSocket gets exactly one reply, correlated by requestId. The base shape is the same for all of them; queries and subscriptions add fields on top of it.
{
// The operation performed
action: "get",
// Data from the operation
data: {
"user123": { name: "John", age: 32 },
"user456": { name: "Jane", age: 28 }
},
// For tracking the request
requestId: "RH8HZX9P",
// Success or Failed
status: "Success"
}
When an error occurs, the response includes:
{
status: "Failed",
action: "set",
requestId: "RH8HZX9P",
message: "Error description here"
}
A rule denial carries no message. When a read/write/validate rule refuses an operation the reply is { status: "Failed", action, requestId } and nothing else — deliberately, so the response cannot be used to probe which rule refused or what data it looked at. A message is present only when the engine itself threw (a bad path, an invalid cursor, a limit exceeded, a non-numeric increment target).
Status values
| Status | Meaning |
|---|---|
"Success" | The operation completed. |
"Failed" | Refused by a rule, or the engine threw. Check for a message. |
"Path not found" | From get — nothing exists at that path. data is null. Note the exact spelling differs by operation: remove answers "path not found", lowercase. |
"Incomplete" | From query — a partial result, stopped against a hard scan limit. truncated is true. Treat the data as valid but not exhaustive. |
"Error" | Server-side calls only. A synchronous call from app.js that threw returns { status: "Error", data: <message>, requestId } rather than throwing at your call site. |
Check status, not just data. Client data methods (get, set, update, increment, remove, query, callableFunction) resolve on failure — they do not reject. An await set(...) that a rule refused looks exactly like a success unless you read status. The file methods are the deliberate exception: they reject with a NukeBaseFileError carrying an HTTP .status.
Query responses
A query that uses any window parameter (orderBy, limit, startAt, cursor, count) carries execution metadata alongside data. A plain query({path, query}) keeps the two-field shape above.
{
status: "Success",
action: "query",
requestId: "RH8HZX9P",
data: { p14: {...}, p3: {...}, p91: {...} },
order: ["p14", "p3", "p91"], // the authoritative ordering — read through this
plan: "index-order", // scan | index-filter | index-order | index-union | index-count
examined: 11, // records the engine had to look at
truncated: false, // true ⇒ status is "Incomplete"
nextCursor: "W1s0MiwicDkxIl0" // absent when there are no more results
}
A count: true query returns data: null and puts the number in count. See Query response metadata.
Subscription payloads
Subscription messages are not request replies: they carry no requestId and are routed to your handler by action + event + path. That includes the first payload, the one delivered immediately on subscribe.
// getSub / getSubChanged
{
status: "Success",
action: "getSub",
event: "value@",
path: ["users", "john"],
data: { name: "John", age: 32 }
}
// querySub / querySubChanged — the window is echoed back so the SDK can
// route the notification to exactly the handler that asked for it.
{
status: "Success",
action: "querySub",
event: "value@",
path: ["players"],
data: { p14: {...}, p3: {...} },
childPath: [], // echoed window ↓
query: "child.active == true",
orderBy: ["score"], // null means "ordered by record key"
desc: true,
limit: 10,
startAt: null, // absent parameters echo as null, not omitted
cursor: null,
order: ["p14", "p3"], // plus plan/examined/truncated/nextCursor
plan: "index-order" // for windowed subscriptions
}
A subscription can fail after it starts. If a querySub grows past 1,000 matching records the server sends a status: "Failed" payload with an explanatory data string and then drops the subscription. Your handler receives that payload like any other — check status inside subscription handlers, not only on request replies. See Subscription Limits.
Cancelling a subscription answers { status: "Success", action: "unSubscribe", requestId }.
Connection-level messages
Two messages come from the connection itself rather than from an operation. Both may arrive with no requestId to correlate, so they surface through the subscription dispatcher rather than settling a pending promise.
| Message | When |
|---|---|
{ status: "Failed", action: "parseError", requestId?, message } |
The message could not be parsed, nested deeper than 64 levels, contained a forbidden key, or was not a JSON object. The server answers rather than dropping it silently — a dropped message leaves the caller's promise pending forever. requestId is echoed when it could still be recovered from the payload. |
{ status: "Failed", action: "rateLimit", message: "Too many messages" } |
The connection exceeded 500,000 messages per second. The socket is closed with code 1008 immediately afterwards. |
Durable writes have one extra failure mode. A { durable: true } write whose disk flush fails returns the normal reply with status overwritten to "Failed" and message: "Durable flush failed" — even though the in-memory state was updated and subscribers already saw it. A retry timer reconciles the disk later. See Durable Writes.
Client requests time out after 30 seconds. If no reply arrives, the promise rejects with Request timeout: <action> <requestId>. This is a client-side timer, not a server response.