Wire protocol
The HTTP envelope, headers, status codes, and SSE event format every Lakefront client and router speaks.
This page is the transport-level reference; procedure input/output shapes live in the wire contract.
POST /rpc/<procedure>
Every read and write goes through POST /rpc/<procedure> with a JSON body matching that procedure’s input schema. <procedure> is one of query.execute, mutate.insert, mutate.update, mutate.delete, changes.read, node.stats. See the contract reference for procedure input and output types.
Envelope discipline
A handler that ran always answers 200, with the outcome in the body:
type WireEnvelope =
{ status: "ok"; value: unknown } | { status: "error"; error: { _tag: string; message: string } };
error._tag is the same discriminant the engine produced: a client branches on it directly rather than reverse-engineering an HTTP status. The full tag list is at the error catalog.
HTTP status codes are reserved for requests that never reached a handler:
| Status | Tag | When |
|---|---|---|
401 |
Unauthenticated |
Missing or invalid bearer token, when auth is on. |
401 |
MissingBoardEnvelope |
No x-lakefront-board header and the token doesn’t pin exactly one board (or auth is off and no header was sent). |
403 |
Forbidden |
Token’s claims don’t allow the procedure’s required verb on this board. |
404 |
UnknownProcedure |
<procedure> isn’t in the contract. |
400 |
InvalidInput |
Body isn’t valid JSON, or fails the procedure’s input schema. |
A non-200 response still carries a JSON body of the same { status: "error", error: { _tag, message } } shape: a client can decode it the same way regardless of which side of the 200 line it landed on.
Headers
| Header | Direction | Notes |
|---|---|---|
content-type |
request/response | application/json on /rpc/*; text/event-stream on /watch. |
x-lakefront-board |
request | The board a call targets. Omissible when the bearer token pins exactly one board (server derives it from the claim). |
authorization |
request | Bearer <token>. Absent entirely when the node runs with auth off. |
x-lakefront-idempotent |
request | Marks a keyed mutation retryable, so the router can hedge/fail over it without parsing the body. Set by clients exactly when the request body carries an idempotencyKey. |
x-lakefront-served-by |
response (router only) | The node id that actually served the request. |
x-lakefront-hedged |
response (router only) | "true" / "false": whether a hedge request was in flight when the winner answered. |
GET /watch (SSE)
Server-sent-events change stream: catch-up from a watermark, then follow.
| Aspect | Value |
|---|---|
| Query param | after: a watermark this engine handed out. Defaults to the node’s current head if omitted. |
| Auth verb | read |
content-type |
text/event-stream |
| Events | change: one { seq, kind, payload, actor? } record. resync: { from }, when the requested range is beyond retention; re-read current state with a query and continue from from. error: { _tag, message }. |
| Heartbeat | A : hb comment line every 15s. |
| Freshness | Writes on this node push to the stream immediately (via an internal change listener). Writes landing on other nodes are picked up by a 500ms poll fallback. The stream is complete either way; the listener only sharpens latency. |
| Lifetime | The stream ends once the token’s exp passes: a stream must not outlive its token by more than one poll interval. |
Tail-only retention, what resync means for a consumer, and how to rebuild are covered in the watch guide.
GET /health
{ ok: true, nodeId }. No auth, no engine work. A cold node still answers.
Board id validity
A board id must match /^[A-Za-z0-9_]{1,64}$/u (core/types.ts, not the contract package; noted here since it gates every request that names a board). Board ids are interpolated into table names, so they’re validated rather than escaped; anything outside this charset is rejected with InvalidBoardId.
Examples
A query:
curl http://127.0.0.1:8080/rpc/query.execute \
-H 'content-type: application/json' \
-H 'x-lakefront-board: acme_roadmap' \
-d '{"sql":"SELECT id, status FROM {{table}} LIMIT 10","tables":[{"token":"table","entity":"items"}]}'
A keyed mutation:
curl http://127.0.0.1:8080/rpc/mutate.insert \
-H 'content-type: application/json' \
-H 'x-lakefront-board: acme_roadmap' \
-H 'x-lakefront-idempotent: 1' \
-d '{"rows":[{"id":"i1","state":"active","title":"first item"}],"idempotencyKey":"create-i1"}'
An SSE subscribe:
curl -N http://127.0.0.1:8080/watch?after=0 \
-H 'x-lakefront-board: acme_roadmap'