The node’s HTTP server (default bind /ip4/127.0.0.1/tcp/2528 and
/ip6/::1/tcp/2528) exposes four
surfaces: a REST-style admin API, a JSON-RPC endpoint for app
execution, a WebSocket stream, and a Server-Sent Events stream. Every
path below is taken directly from the axum routers in the server crate.
All four surfaces are mounted on a single HTTP server and wrapped by the same
optional auth layer — when embedded auth is on, every one of them is guarded
identically; when it is off, all four are open. They differ in shape, not in how
they authenticate:
Surface
Path
Shape
Use it for
Admin REST
/admin-api
Request/response
Node management: install apps, create contexts and groups, manage members and aliases, inspect state.
JSON-RPC
/jsonrpc
Request/response
One-shot app calls. execute (run a method) and sync_status.
WebSocket
/ws
Bidirectional, long-lived
App calls plus live updates over one connection: execute, subscribe, unsubscribe.
SSE
/sse
Server→client stream
A read-only event feed when you only need to receive events (no custom request headers needed).
Two practical notes:
There is one execute, reached two ways. The execute over JSON-RPC and the
execute over WebSocket resolve to the same execution kernel — same
executor resolution, same runtime invocation, same result envelope. Pick
JSON-RPC for a single stateless call; pick WebSocket when you also want to
subscribe to events on the same connection.
Query vs. mutate is not a flag you set. Whether an execute call only
reads state or commits a change is decided by the target method’s own
#[app::view] declaration in the app, not by the transport or any request
field. The same execute request shape covers both.
The server applies one CORS layer to every mounted route. It allows Any
origin, Any request header, the methods GET/POST/PUT/PATCH/DELETE/OPTIONS,
and sets allow_private_network(true) so requests from a public-origin page to
a loopback node pass the browser’s private-network preflight.
Because the response uses Any origin, allow_credentials is intentionally
not set (the two are incompatible per the CORS spec, so enabling it would be a
no-op for browsers). Cookie/credentialed auth therefore does not work against
the default configuration; pass tokens via the Authorization header instead.
Cross-origin clients (browser SPAs, Tauri webviews) can only read response
headers that the server explicitly exposes. The layer exposes exactly three:
Exposed header
Use
x-auth-error
Signals the auth-failure reason. A 401 from an expired-but-refreshable token carries X-Auth-Error: token_expired.
x-auth-user
The authenticated user/key.
x-auth-permissions
The caller’s resolved permissions.
To implement transparent refresh-on-401, read X-Auth-Error on a 401: if it is
token_expired, refresh the access token and retry; otherwise treat it as a
hard auth failure. If these headers are not exposed (e.g. behind a proxy that
strips them), every access-token expiry surfaces as a hard logout instead.
WebSocket upgrade. Carries execute, subscribe, and unsubscribe messages over the socket.
When embedded auth is enabled, the auth guard runs at the HTTP upgrade
request, before the socket is established — an unauthenticated upgrade is
rejected with a 401 and never becomes a WebSocket. The guard resolves the token
in this order:
An Authorization: Bearer <jwt> header. If present, it is validated
exclusively — even an invalid token here is rejected rather than retried.
Only if no Authorization header is present, a ?token=<jwt> query
parameter. This fallback exists because the browser WebSocket and
EventSource APIs cannot set custom headers.
If the Authorization header is present, the ?token= query parameter is
ignored entirely.
The SSE stream uses a skip-on-disconnect delivery model: there is no replay
buffer. Sessions persist (subscriptions and a monotonic event counter survive a
reconnect), but any event emitted while a client is disconnected is lost and
never redelivered. Event IDs are sequential, so a gap in the IDs a client
receives is the signal that events were missed.
On connect the response carries two headers for the client to track its session:
Response header
Meaning
X-SSE-Session-ID
The session ID assigned to (or resumed by) this stream.
X-SSE-Reconnect
true if this connection resumed an existing session, false for a fresh one.
To reconnect, the client sends the Last-Event-ID header. Event IDs have the
format {session_id}-{n}; the server parses the session_id prefix to restore
the session. Sessions expire after 24 hours of inactivity, after which a
reconnect attempt starts a fresh session. The stream also advertises a retry
hint of 3000ms in its initial event.