Node roles and factories
The three node factories are service-layer constructors. They expose the protocol roles to a host application without choosing an ASGI server, route layout, process model, or deployment topology.
WebSocket role
The WebSocket role authenticates the initial hello, negotiates the supported protocol, registers the logical client and current runtime, maintains heartbeat evidence, owns the live socket, delivers remote commands, and accepts command ACKs. A Redis registry records where a connection lives, but the actual WebSocket handle remains in its owning process.
The preferred form when a preset carries composition constraints is:
websocket_node = create_websocket_node(
components=components,
config=WebSocketNodeConfig(
rest_message_endpoint="https://api.example.com/cheetah/messages",
),
)
The low-level form requires auth, registry, correlator, tracer, and config. dispatcher and node_id are optional in the signature, but they are required for the full coordinated reconnect and cross-node delivery behavior. Do not mix components= with individual dependencies.
WebSocketNodeConfig requires the externally usable REST message endpoint. Its defaults are:
| Setting | Default | Meaning |
|---|---|---|
heartbeat_interval_ms | 25000 | heartbeat interval advertised to the client |
protocol_version | "1.0" | canonical wire version |
supported_protocol_versions | only the canonical version | exact accepted-version set; not a range |
transport_security_mode | strict | reject insecure or undetermined external posture |
server_config | None | no additional advertised client configuration |
require_tenant | False | ordinary dedicated-principal mode |
Strict mode requires a secure advertised return endpoint and trustworthy knowledge of the external scheme. Behind a proxy, configure the adapter and proxy trust boundary correctly; an internal plaintext hop does not by itself tell Cheetah that the public connection was secure. debug_insecure is for explicit controlled development, with a client that also opts in to insecure transport.
REST role
The REST role accepts authenticated messages and client state returning over HTTP. It validates and fences responses, deduplicates accepted deliveries, writes retained evidence, wakes response waiters, resolves qualifying terminal RPCs, and updates optional topology registries.
create_rest_node() requires registry, deduplicator, history, correlator, tracer, and the keyword-only response_notifier. A token signer and the context and browser-window registries are optional. The notifier is not optional composition glue: pass the compatible member from the same component family that supplies the App role.
RestNodeConfig.max_ingest_bytes defaults to 1 MiB and can be set to None to disable the node-level limit, which is not recommended. Align the reverse proxy and web framework limits with this boundary. require_tenant defaults to False and is enabled by the dedicated shared-tenancy composition.
RestNode is not an HTTP server. The host endpoint must establish the trusted user or tenant principal and call the service with that authenticated context. The client-supplied message does not get to choose its storage or identity scope.
App role
The App role is the surface product code calls. It authorizes and addresses commands, checks connection evidence, admits work to the dispatcher, registers terminal correlation, reports delivery evidence, waits for retained responses, reads history and presence, and supports signed payload references when configured.
create_app_node() requires registry, dispatcher, correlator, history, tracer, and the keyword-only response_notifier. A token signer, central policy source, and parser registry are optional. REST and App must use compatible notifier implementations in the same deployment namespace; otherwise newly retained responses cannot reliably wake the intended application waiter.
The important AppNodeConfig defaults are:
| Setting | Default | Boundary |
|---|---|---|
default_command_timeout_ms | 30000 | terminal observation after a send attempt binds a runtime |
default_reconnect_wait_ms | 45000 | bounded wait for a recently seen logical client to reconnect |
max_reconnect_wait_ms | 300000 | maximum caller-selected reconnect wait |
default_max_client_staleness_ms | 180000 | default age limit for retained sighting evidence |
max_client_staleness_ms | 86400000 | maximum configurable sighting age |
connection_freshness_ms | 50000 | age below which an active record is immediately usable |
minimum_delivery_budget_ms | 10000 | time reserved for durable admission after reconnect waiting |
max_connection_waiters | 1024 | App-process-wide pre-admission waiter capacity |
max_connection_waiters_per_tenant | 128 | tenant capacity in shared mode; effective-user capacity otherwise |
max_ws_message_bytes | None | no App-side command-size limit unless the host selects one |
require_tenant | False | ordinary dedicated-principal mode |
The staleness ceiling cannot exceed the registry's retained-sighting lifetime. Treat these values as one admission policy, not as interchangeable timeouts.
Complete one-process composition
from servercheetah.presets import create_dev_components
from servercheetah.servers import (
AppNodeConfig,
RestNodeConfig,
WebSocketNodeConfig,
create_app_node,
create_rest_node,
create_websocket_node,
)
from servercheetah.types.messages import TransportSecurityMode
components = create_dev_components(
node_id="development-1",
api_keys={"development-key": "development-user"},
enable_worker_topology=True,
)
websocket_node = create_websocket_node(
components=components,
config=WebSocketNodeConfig(
rest_message_endpoint="http://127.0.0.1:8080/cheetah/messages",
transport_security_mode=TransportSecurityMode.debug_insecure,
),
)
rest_node = create_rest_node(
registry=components.registry,
deduplicator=components.deduplicator,
history=components.history,
correlator=components.correlator,
tracer=components.tracer,
response_notifier=components.response_notifier,
context_registry=components.context_registry,
browser_window_registry=components.browser_window_registry,
config=RestNodeConfig(),
)
app_node = create_app_node(
registry=components.registry,
dispatcher=components.dispatcher,
correlator=components.correlator,
history=components.history,
tracer=components.tracer,
response_notifier=components.response_notifier,
parser_registry=components.parser_registry,
config=AppNodeConfig(),
)
This example deliberately uses an insecure local endpoint and a fixed development credential. It is a local composition pattern, not a production security example.
What factories do not provide
The returned services do not mount framework routes, bind sockets, start component listeners, create worker managers, authenticate administrative pages, install TLS, expose readiness, or close caller-owned dependencies. A host may place the three roles together or separately, but every process must still manage the lifecycle of the component set it constructed.
Continue to Lifecycle, readiness, and ownership or use Application API for the product-facing surface.