# 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: ```http POST /data/get Content-Type: application/json { "resourceId": ["resource-a", "resource-b"] } ``` Write: ```http POST /data/write Content-Type: application/json { "resources": [ { "id": "resource-a", "publicKey": "...hex...", "timestamp": 1730000000000, "signature": "...der-hex...", "value": "" } ] } ``` Write response: ```json { "resources": [ { "id": "resource-a", "publicKey": "...hex...", "blob": "", "timestamp": 1730000001000 } ] } ``` If any item has invalid authorization, the whole batch fails. ## SSE Subscriptions The client explicitly requests SSE using `Accept: text/event-stream`: ```http 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: ```text id: 1730000001000 event: instance-changed data: {"resourceId":"resource-a"} ``` After SSE begins, failures arrive as error events and the server closes that SSE connection: ```text 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: ```ts 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: ```ts type WsRequest = { id?: string; path: string; body?: unknown; }; ``` Old fields such as `event`, `type`, `url`, `method`, `headers`, `params`, and `data` are rejected. Read: ```json { "id": "read-1", "path": "/data/get", "body": { "resourceId": ["resource-a", "resource-b"] } } ``` Write: ```json { "id": "write-1", "path": "/data/write", "body": { "resources": [ { "id": "resource-a", "publicKey": "...hex...", "timestamp": 1730000000000, "signature": "...der-hex...", "value": "" } ] } } ``` Subscribe: ```json { "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: ```json { "id": "unsub-1", "path": "/data/unsubscribe", "body": { "resourceId": ["resource-a"] } } ``` Unsubscribe is idempotent. The response is a normal correlated response: ```json { "id": "unsub-1", "type": "response", "statusCode": 200, "body": {} } ``` Normal response: ```json { "id": "write-1", "type": "response", "statusCode": 200, "body": {} } ``` Error response: ```json { "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: ```json { "id": "server-event-id", "type": "instance-changed", "data": {} } ``` ## Signing For each written resource, sign this canonical payload: ```ts `${timestamp}${resourceId}${canonicalBody(value)}`; ``` `value` is the `Uint8Array` payload. `canonicalBody(value)` uses the same Extended JSON rules as the server: ```json "" ```