---
title: Wire protocol
description: 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](/reference/contract).

## POST /rpc/&lt;procedure&gt;

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](/reference/contract) for procedure input and output types.

## Envelope discipline

A handler that **ran** always answers `200`, with the outcome in the body:

```ts
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](/reference/errors).

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](/guides/watch).

## 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:

```bash
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:

```bash
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:

```bash
curl -N http://127.0.0.1:8080/watch?after=0 \
  -H 'x-lakefront-board: acme_roadmap'
```
