How subscriptions work
Trax's subscriptions run on HotChocolate's subscription infrastructure with its in-memory pub/sub
transport. AddTraxGraphQL() enables them; the fields, payloads and authentication are in the
Subscriptions reference.
Lifecycle events come from a hook
Subscriptions are powered by the lifecycle hooks system. The GraphQLSubscriptionHook is automatically registered by AddTraxGraphQL() and publishes lifecycle events to HotChocolate's in-memory subscription transport.
At startup, the hook builds a set of canonical train names (using ServiceType.FullName, the interface name) from registrations that have [TraxBroadcast]. On each lifecycle event it publishes when the train is opted in. If the operations surface is exposed, it publishes for every train (TrainLifecycleStreamOptions.StreamAllTrains, set automatically by AddTraxGraphQL() when ExposeOperationQueries()/ExposeOperationMutations() is called).
ServiceTrain.Run()
→ LifecycleHookRunner.OnCompleted()
→ GraphQLSubscriptionHook.OnCompleted()
→ Publish if: operations surface exposed (all trains), OR
metadata.Name matches a [TraxBroadcast] train's ServiceType.FullName
→ Yes → ITopicEventSender.SendAsync("OnTrainCompleted", event)
→ WebSocket clients receive the event
→ No → skip (no event published)Across processes
By default, subscriptions only fire for trains that execute in the same process as the GraphQL API. In distributed deployments where trains are queued and executed by separate worker processes, use UseBroadcaster() to bridge the gap:
// Both hub and worker:
effects.UseBroadcaster(b => b.UseRabbitMq("amqp://guest:guest@localhost:5672"))When a broadcaster is configured, AddTraxGraphQL() automatically registers a GraphQLTrainEventHandler that receives remote lifecycle events from the message bus and forwards them to HotChocolate's subscription transport. It applies the same rule as the local GraphQLSubscriptionHook: forward every train when the operations surface is exposed, otherwise only [TraxBroadcast] trains (matched by canonical ServiceType.FullName), regardless of which process executes them. Events from the local process are de-duplicated automatically.
Data-change signals ride the same bridge. In a single-process deployment (the API collocated with the scheduler), onDataChanged works with no broadcaster: signals flow in-process to the subscription topic. When the scheduler runs in a separate process, UseBroadcaster() forwards its Notify calls over the message bus (a BroadcastChangeSink), and the API's GraphQLDataChangeHandler re-publishes them to local subscribers. The originating host ignores its own broadcast via the same instance-id de-duplication used for lifecycle events, so replicas of one app still receive each other's signals.
See UseBroadcaster for full details.
No replay, so clients refetch
The server keeps no state per subscriber and replays nothing: an event emitted while a client's socket is down is not delivered later. A client that has to stay correct across a restart or a network drop therefore does two things, and the Trax dashboard does both.
It reconnects for as long as the failure is transient. graphql-ws gives up after 5 attempts by
default, and the dashboard sets retryAttempts: Infinity. Each reconnect re-establishes the active
subscriptions and authenticates again, because the credential rides in connection_init. An
auth-rejection close code (4400, 4401, 4403, 4429) is not retried: a retry cannot fix a bad
credential, so the client terminates the socket and opens a new one with a new credential.
It refetches what it shows once the socket is connected again, since whatever changed while it was
down is gone from the feed. While connected, the coalesced onDataChanged signal drives the
refetches; the one after a reconnect covers the gap.
Registration order
Subscription authentication does not depend on whether the AddTrax*Auth call comes before or after AddTraxGraphQL(). Earlier versions chose the interceptor from what was registered when AddTraxGraphQL() ran, so an auth call placed after it left subscriptions accepting every connection while HTTP stayed gated, and a later version refused to start instead. The composite reads the registered schemes once the container is complete, so either order authenticates.
If a token scheme is registered and HotChocolate's own accept-everything interceptor is the active one, the host refuses to start. Only registering DefaultSocketSessionInterceptor yourself produces that.