Protocol
WebSocket protocol V1 specification for building FlowLayer clients. Everything needed to connect, authenticate, send commands, and consume events in any language.
Session Model
Each WebSocket connection is an independent session. Sessions are ephemeral — there is no server-side persistence and no automatic replay after reconnection.
ws://<host>:<port>/wsget_logs.Authentication
vpn_key Bearer token on upgrade request
Authentication happens at the HTTP level during the WebSocket upgrade, before the connection is established.
| Missing Authorization header | HTTP 401 |
| Invalid token | HTTP 403 |
| Valid token | Upgrade proceeds |
Session API Endpoints
favorite GET /health
Authenticated liveness check. Returns {'\"ok\": true'} when the server is running.
cable GET /ws
WebSocket upgrade endpoint. All protocol V1 communication happens over this connection.
Message Envelope
Every message in both directions follows this structure. Fields use omitempty — absent when not applicable.
{ "type": "<message_type>", "id": "<correlation_id>", "name": "<command_or_event_name>", "payload": {} }
type Always required One of: command, ack, result, event, error
id command, ack, result Correlation identifier. UUID recommended. Used to match ack and result to the originating command.
name command, event Command or event name, e.g. get_logs, service_status.
payload Varies Content specific to the message type. Absent when not applicable.
Message Types
command Client → Server Client request. Requires a unique id and a name. Routed to a matching handler.
ack Server → Client Immediate acknowledgment. Private to the sending client. Contains accepted boolean. If accepted = false, the rejection is final for that command and no result follows.
result Server → Client Final command outcome after an accepted ack. Private to the sending client. Only sent when ack.accepted = true, including runtime failures with ok = false.
event Server → Client Asynchronous notification. Runtime events (service_status, log) are broadcast to all connected sessions. Handshake events (hello, snapshot) are private to the connecting session.
error Server → Client Protocol-level error. No correlation — not tied to a specific command.
Command Flow
command with unique idack for that idresult for that id// Accepted { "accepted": true, "error": null } // Rejected { "accepted": false, "error": { "code": "unknown_service", "message": "..." } }
// Success { "ok": true, "data": { ... }, "error": null } // Failure { "ok": false, "error": { "code": "...", "message": "..." } }
If ack.accepted = false, that rejected ack is the final response for the command. No result follows.
If ack.accepted = true, the server later sends result for the same id, with ok = true or ok = false.
Set a client-side timeout (5 seconds is reasonable). The server does not impose command timeouts. If no response arrives, treat the command as failed.
Commands
get_snapshot No payload Returns the current state of all services. Services are sorted alphabetically.
// result.data { "services": [ { "name": "billing", "status": "ready" }, { "name": "users", "status": "starting" } ]}
get_logs Optional payload Returns log entries stored in memory.
Request fields (all optional)
serviceFilter by service name. Omit for all services.limitMax entries. Omit to let server decide (uses logView config).after_seqOnly return entries with seq > this value.Response fields
entriesLog entries ordered by seq.truncatedtrue if entries were dropped to fit within limit.effective_limitLimit actually applied. Use this for buffer sizing.// Entry fields { "seq": 42, // global monotonic sequence number "service": "billing", "phase": "running", // informational lifecycle phase "stream": "stdout", // or "stderr" "message": "GET /healthz 200", "timestamp": "2026-04-19T09:21:11Z" }
Limit resolution when client omits limit:
All-services: logView.all.maxEntries → logView.maxEntries → 500
Per-service: services.<name>.logView.maxEntries → logView.maxEntries → 500
Truncation: without after_seq, tail truncation (last N entries). With after_seq, entries with seq > after_seq, then tail truncation.
start_service stop_service restart_service payload: {service: string} Start, stop, or restart a named service. May be rejected with unknown_service or service_busy. Both come as ack.accepted = false.
{"type": "command", "id": "...", "name": "restart_service", "payload": {"service": "billing"}}
start_all stop_all No payload Start or stop all services.
Events
hello Handshake · Private First event on connection. Announces the protocol version and available commands.
{ "protocol_version": 1, "server": "flowlayer", "capabilities": ["get_snapshot", "get_logs", "start_service", "stop_service", "restart_service", "start_all", "stop_all"] }
snapshot Handshake · Private Full initial service state. Sent immediately after hello. Use this as the authoritative starting point; then apply incremental service_status events.
{ "services": [{ "name": "billing", "status": "ready" }, ...] }
service_status Runtime · Broadcast Service state transition. Broadcast to all connected clients. Apply as incremental update on top of the initial snapshot.
{ "name": "billing", "status": "ready" }
log Runtime · Broadcast · Best-effort Live log entry. Broadcast to all connected clients. Delivery is best-effort — slow clients may miss events. Deduplicate by seq and use get_logs with after_seq to recover gaps.
{ "seq": 42, "service": "billing", "phase": "running", "stream": "stdout", "message": "GET /healthz 200", "timestamp": "2026-04-19T09:21:11Z" }
Service States
starting Server accepted start request. Launch sequence is in progress. Not yet operational.
running Process spawned. Terminal state for daemons without a readiness probe, and for oneshots on exit 0. Transitional for daemons with a probe.
ready Readiness probe passed. Service is considered fully operational. Terminal startup state for services with a probe.
stopping Stop sequence is in progress. Concurrent actions on this service are rejected.
stopped Service has terminated. Process group no longer active and port (if any) released.
failed Startup or shutdown failed. Clients should surface this to the user.
unknown No state recorded yet. Appears only in snapshot responses for services not yet started.
running does not always mean full operational readiness. For services without a readiness probe, running is the terminal startup state. For services with a probe, running is transitional — only ready indicates full availability.
Error Codes
Errors can appear either as an error message or as a rejected ack with accepted = false. If a command is accepted and later fails during execution, the failure appears in result with ok = false. The error message type is used for protocol-level failures before a command is parsed; ack carries command rejection; result carries runtime failure after acceptance.
| Code | Where | Meaning |
|---|---|---|
invalid_json | error | Message could not be parsed as JSON |
malformed_message | error | Message parsed but required envelope fields are missing or wrong type |
unknown_command | ack | Command name not recognized by the server |
invalid_payload | ack | Command payload is structurally invalid or missing required fields |
unknown_service | ack | Service name not found in config |
service_busy | ack | Service is mid-start or mid-stop; concurrent action rejected |
internal_error | result | Unexpected server-side error |
Building a Client
The only requirements for building a FlowLayer client are a WebSocket library and a JSON parser. The official TUI is the reference implementation. Below is the minimal client checklist.
Authorization: Bearer <token>hello and snapshot before sending commandstype, then by name for eventsack / result to commands by idlastSeq for log continuityseqeffective_limit from get_logs for buffer sizingafter_seqwarning Log management complexity
Log management is the most complex part of a FlowLayer client. Getting it wrong leads to missing logs, duplicates, or unbounded memory growth. Key rules:
- →Never assume a fixed limit — always use
effective_limitfrom the server response - →Without
after_seq, the server returns the last N entries (tail truncation) - →Live
logevents are best-effort; gaps are normal under load
build Reference implementation
The TUI is the reference client. Its log deduplication, reconnect logic, and state merging are a practical example of correct protocol usage.
github.com/FlowLayer/tui open_in_new