This document describes the Fitz lifecycle from the client's point of view.
It intentionally omits broker-internal routing, shard assignment, and worker
dispatch details. Clients interoperate through transports, CONNECT(jwt), and
documented domain payloads only.
Every Fitz session follows the same high-level flow:
- Open a WebSocket or TCP connection.
- Send
CONNECTwith the JWT as the first Fitz message. - Treat a still-open connection as success and a close frame/socket close as failure.
- Send domain requests that contain routes plus documented payload fields.
- Decode synchronous responses and asynchronous deliveries.
- On reconnect, authenticate again and rebuild any connection-scoped state.
- Connect to
ws://orwss://. - Complete the normal WebSocket upgrade handshake.
- Send Fitz payloads in binary frames only.
- Reject or ignore text-frame based integrations; Fitz is binary only.
- Connect to the configured broker port.
- Frame each Fitz message as
[u32 BE length][payload bytes]. - Buffer reads until the full payload is available.
- Reject frames whose declared length exceeds the configured maximum.
Both transports carry the same Fitz payloads. A client should decode the same message body the same way regardless of whether it arrived over WebSocket or TCP.
The client first establishes the transport connection:
client = FitzClient.connect("wss://broker.example/ws")At this stage the transport is open, but the Fitz session is not authenticated yet.
The first Fitz message on a new connection must be CONNECT with the compact JWT
in the payload:
client.connect(jwt_token)Wire shape:
[MessageType=1][Length=N][jwt bytes]
Client requirements:
CONNECTMUST be the first Fitz message on a new connection.- The client MUST NOT send extra shard or routing metadata.
- The client MUST treat the JWT as authentication input, not as a source of client-side dispatch logic.
Fitz uses silent success for CONNECT:
- Success: the connection stays open and subsequent domain operations succeed.
- Failure: the broker closes the connection and may include a reason such as
connect failed: <reason>.
There is no explicit CONNECT_OK response frame to wait for.
After a valid CONNECT, the broker may attach internal session metadata and
authorization state. In authenticated mode the broker resolves route family
server-side from the configured identity claim and FITZ_ROUTE_FAMILY_MAP;
anonymous mode always uses internal family 1.
Clients do not observe or manage that state directly.
Once authenticated, the client sends domain requests. Each request is self-contained and includes the route plus the fields documented for that message type.
User-facing call:
tx = client.kv_begin(
route="kv://prod/app/users",
mode=TxMode.ReadWrite,
durability=Durability.Sync,
)Wire payload shape:
[MessageType=100][Length=N][route][mode][durability]
Client-side processing:
- Encode the route string.
- Encode the operation fields in documented order.
- Send the frame over WebSocket or TCP.
- Wait for the response frame.
- Decode the response and return a transaction object or error.
Response handling:
tx.put(b"user:123", b"alice")
tx.commit()The transaction object may store route and transaction identifiers internally so the public API stays ergonomic, but each wire operation still carries its documented fields.
- Routes are opaque strings from the client's perspective.
- Clients MUST NOT derive broker dispatch behavior from JWT claims or route segments.
- Clients MUST NOT add undocumented routing fields to request payloads.
- Clients SHOULD surface domain errors as typed client errors when possible.
Some domains deliver messages asynchronously after a client subscribes or registers:
- Notice:
NOTIFY - RPC: inbound worker requests and responses
- Stream: subscription deliveries
- Schedule: notifications
Typical flow:
- Client sends a
SUBSCRIBE-style request. - Broker acknowledges according to that domain's response contract.
- Client keeps a handler or callback registered for future deliveries on that connection.
- Incoming delivery frames are decoded using the same protocol layer as normal responses.
Example:
const sub = await client.notice.subscribe("notice://prod/app/*", handler);The client should track these registrations per connection because they are not preserved across reconnects.
Disconnects create a new Fitz session. On reconnect, the client must rebuild any connection-scoped state explicitly.
Recommended reconnect sequence:
- Detect socket close, read failure, or write failure.
- Open a new WebSocket or TCP connection.
- Send
CONNECT(jwt)again. - Re-create subscriptions and worker registrations.
- Resume normal request flow.
State handling rules:
- Subscription state is lost on disconnect.
- Worker registrations are lost on disconnect.
- In-flight KV transactions are lost on disconnect.
- In-flight queue lease handling must be restarted according to the domain contract.
- The client SHOULD assume every reconnect is a brand-new authenticated session.
Clients should handle these cases explicitly:
CONNECTrejected: close the connection and surface the broker reason.- Domain frame sent before
CONNECT: expect connection close. - Malformed or unsupported stream
Readfilter: expect a typed stream error response (2006 or 2007) and keep the connection open. - Partial TCP frame: keep buffering until complete or the socket closes.
- Oversized frame length: fail fast before allocating.
- Reconnect while work is in flight: treat in-flight work as interrupted unless the domain contract says otherwise.
- Support both WebSocket and TCP transports.
- Send
CONNECTfirst on every new connection. - Treat routes as opaque strings.
- Encode only the documented domain fields.
- Decode both synchronous responses and asynchronous deliveries.
- Re-subscribe or re-register after reconnect.
- Never expose broker-internal shard or routing state in the public client API.