5.3 KiB
Demo App Transport Migration
This guide describes the client-facing protocol for the route-agnostic transport architecture.
Connection Model
HTTP, SSE, and WebSocket do not authenticate at the transport layer. Do not send
request-auth headers such as X-PublicKey, X-Signature, or X-Timestamp
for normal route dispatch.
Authentication is scoped to each resource operation. Every written resource
carries its own publicKey, timestamp, and signature.
All route input belongs in the request body. Paths are exact route names; there are no path parameters, query parameters, or alternate HTTP methods.
Request Size Limit
The server accepts at most 1048576 encoded bytes per HTTP request body or
complete WebSocket message by default. Deployments can change this with
SERVER_MAX_REQUEST_BODY_BYTES. The count is over the Extended JSON wire text,
so hexadecimal Uint8Array values use approximately twice their decoded byte
length.
HTTP limits use Hono's built-in bodyLimit middleware and return 413.
WebSocket messages do not pass through HTTP middleware; Hono's Node adapter uses
ws, whose native maxPayload option rejects oversized frames before the
application message handler. Response bodies are not limited.
HTTP Requests
All application HTTP routes use POST.
Read:
POST /data/get
Content-Type: application/json
{
"resourceId": ["resource-a", "resource-b"]
}
Write:
POST /data/write
Content-Type: application/json
{
"resources": [
{
"id": "resource-a",
"publicKey": "...hex...",
"timestamp": 1730000000000,
"signature": "...der-hex...",
"value": "<Uint8Array: deadbeef>"
}
]
}
Write response:
{
"resources": [
{
"id": "resource-a",
"publicKey": "...hex...",
"blob": "<Uint8Array: deadbeef>",
"timestamp": 1730000001000
}
]
}
If any item has invalid authorization, the whole batch fails.
SSE Subscriptions
The client explicitly requests SSE using Accept: text/event-stream:
POST /data/subscribe
Accept: text/event-stream
Content-Type: application/json
{
"resourceId": ["resource-a"]
}
A subscription without this Accept header receives a 406 JSON error.
The server keeps the SSE request open while its resource topics remain
subscribed. Subscribing sends no current state; it only enables future
publications. Request current state separately through /data/get when needed.
SSE publication:
id: 1730000001000
event: instance-changed
data: {"resourceId":"resource-a"}
After SSE begins, failures arrive as error events and the server closes that SSE connection:
event: error
data: {"statusCode":500,"error":"Internal Server Error"}
To change an SSE subscription, abort the existing request and open a new
/data/subscribe request with the complete desired resourceId list.
WebSocket
Connect without a connection-auth message:
const ws = new WebSocket("ws://host:port/ws");
Every client message uses this strict envelope. Messages may execute
concurrently; the server binds id and body to each individual dispatch so
responses remain correctly correlated even when they finish out of order:
type WsRequest = {
id?: string;
path: string;
body?: unknown;
};
Old fields such as event, type, url, method, headers,
params, and data are rejected.
Read:
{
"id": "read-1",
"path": "/data/get",
"body": {
"resourceId": ["resource-a", "resource-b"]
}
}
Write:
{
"id": "write-1",
"path": "/data/write",
"body": {
"resources": [
{
"id": "resource-a",
"publicKey": "...hex...",
"timestamp": 1730000000000,
"signature": "...der-hex...",
"value": "<Uint8Array: deadbeef>"
}
]
}
}
Subscribe:
{
"id": "sub-1",
"path": "/data/subscribe",
"body": {
"resourceId": ["resource-a"]
}
}
The subscribe request sends no immediate message. Its dispatch remains active
until those topics are removed or the connection closes. Request current state
separately through /data/get when needed.
The client should consolidate its local subscribers and send only the topic set needed by the shared WebSocket.
Unsubscribe selected resources without closing the socket:
{
"id": "unsub-1",
"path": "/data/unsubscribe",
"body": {
"resourceId": ["resource-a"]
}
}
Unsubscribe is idempotent. The response is a normal correlated response:
{
"id": "unsub-1",
"type": "response",
"statusCode": 200,
"body": {}
}
Normal response:
{
"id": "write-1",
"type": "response",
"statusCode": 200,
"body": {}
}
Error response:
{
"id": "write-1",
"type": "error",
"statusCode": 400,
"error": "Validation Error",
"details": []
}
Message-level errors do not close the WebSocket. The client decides whether to retry, alter its subscription, or reconnect.
Published events retain the shared stream shape:
{
"id": "server-event-id",
"type": "instance-changed",
"data": {}
}
Signing
For each written resource, sign this canonical payload:
`${timestamp}${resourceId}${canonicalBody(value)}`;
value is the Uint8Array payload. canonicalBody(value) uses the same
Extended JSON rules as the server:
"<Uint8Array: deadbeef>"