Wire protocol & non-TS SDKs
The language-independent Lunora client↔server wire protocol, its conformance fixtures, and the Python SDK built on it.
Last updated:
The Lunora client surface is TypeScript-first (@lunora/client, @lunora/react,
…), but the wire protocol itself is not TypeScript-bound. It is specified as a
language-independent contract so an SDK in any language can talk to a Lunora
deployment — the first of which is a minimal Python client.
The specification
The normative spec lives at protocol/README.md
in the repository. It covers every layer a client needs:
- Transport map —
POST /_lunora/rpcfor query/mutation/action, theGET /_lunora/wsWebSocket for live subscriptions, and the SSE framing for HTTP streaming routes. - The wire value codec (
encodeWire/decodeWire) — howbigint, bytes,Date,Map/Set,URL,NaN/Infinity, and array-positionundefinedare tagged so they survive JSON, while pure-JSON values stay byte-identical. - The stable subscription key — the sorted, canonical encoding that
de-duplicates
(functionPath, args, shardKey). - The RPC envelopes — request body, success/
commitCursor/lastMutationId, and the{ error: { code, message, data } }failure shape. - The WebSocket frames —
connect,subscribe/unsubscribe, theack/data/delta/resume/settled/errorserver frames, and thepokeStart/pokePart/pokeEndshape (partial-replication) protocol. - Auth — the HTTP bearer, the WS
?token=credential, and the short-lived ephemeral WS admin token minted atPOST /_lunora/admin/ws-token.
Conformance fixtures
The spec is backed by a set of golden frames under
protocol/fixtures/:
wire-codec.json, stable-wire-key.json, rpc.json, and ws-frames.json. Both
the reference TypeScript client and every non-TS SDK are tested against these
identical files, so a protocol drift is caught on both sides:
- TS reference:
packages/client/__tests__/protocol-conformance.test.ts - Python:
sdks/python/tests/test_conformance.py
An SDK is protocol-conformant when it round-trips every codec fixture
(encode(decode(x)) == x), reproduces every stable key, builds/parses the RPC
envelopes, and produces/consumes the WebSocket frames — including materialising
the poke sequence into the expected rowset.
The Python SDK
A minimal, standard-library-only client lives at
sdks/python/:
import asyncio
from lunora import LunoraClient, WireBigInt
async def main():
client = LunoraClient(
url="https://my-app.example.com",
auth_token="…", # HTTP bearer (optional)
ws_token=lambda: mint_ephemeral(), # str | callable | async callable
)
# HTTP RPC round-trips
messages = await client.query("messages:list", {"channel": "general"})
await client.mutation("messages:send", {"channel": "general", "text": "hi"})
await client.mutation("ledger:add", {"amount": WireBigInt(1000)}) # v.bigint()
# Live subscription over the WebSocket
client.subscribe("messages:list", {"channel": "general"}, print)
await client.connect_and_run() # needs `pip install websockets`
asyncio.run(main())It implements query/mutation, live subscribe (data/delta frames) and
subscribe_shape (poke frames), an async WS token provider mirroring
WsTokenProvider, and the full value codec. Because Python lacks TypeScript's
distinct bigint/Map/Set/Date types, those are marked with small
wrappers (WireBigInt, WireDate, WireMap, WireSet, WireUrl,
WireBytes); plain values map to JSON directly.
See sdks/python/README.md
and the runnable examples/quickstart.py.
The Python SDK is a v1 proof that the model ports cleanly. Optimistic updates, the offline queue, and codegen'd typed bindings are follow-ups (see the plan's Phase 2) rather than part of this first release.