6. Replace HTTP Command Handling with Message Queue
- Date: 2026-05-25
- Status: Proposed
Context
SimopsConnect exposes four commands over HTTP:
state: returns current flight session state (userId, isStarted)start: begins a flight session, bound to the authenticated userIdstop: ends the sessiondebug-mq: publishes a test message to RabbitMQ
All HTTP commands require a JWT Bearer token in the Authorization header. The token is validated via JWKS, and the sub claim is extracted as the userId, which flows through StateService and FlightInfoService. The app already publishes outbound telemetry to RabbitMQ (flights, flightevents exchanges).
The goal of this change is to replace the inbound HTTP command channel with a message queue, so that commands are decoupled from the HTTP request/response cycle while preserving the authentication requirement (userId must be known and verified for every command).
Decision
Replace the HTTP command listener (HttpService.cs) with an AMQP command consumer backed by RabbitMQ or LavinMQ, using JWT embedded in AMQP message headers as the authentication mechanism.
The state query will be removed from the command channel and instead served by the existing WebSocket broadcast (already emitting flight state to connected clients).
Options Considered
Option A: RabbitMQ (extend existing infrastructure)
- Already integrated for outbound publishing.
RabbitMQ.ClientNuGet package already present — no new dependencies.- JWT placed in AMQP
IBasicProperties.Headers["Authorization"], validated by the same JWKS logic currently inHttpService.cs. - Estimated effort: 4.5 – 6 days.
Option B: LavinMQ
- AMQP 0-9-1 wire-compatible with RabbitMQ,
RabbitMQ.Clientworks unchanged. - Written in Crystal, lower memory/CPU footprint, relevant since the host machine also runs Flight Simulator.
- No streams, quorum queues, or plugin ecosystem: none of which this app uses.
- Migration is infrastructure-only (swap broker endpoint in
appsettings.json). - Same code effort as Option A: swap is operationally trivial.
Option C: Azure Service Bus
- Azure App Configuration is already in use, so Azure is partially integrated.
- Natural alignment if the JWT issuer is Azure AD.
- Requires new Azure resource, new SDK (
Azure.Messaging.ServiceBus). - Estimated effort: 6 – 7.5 days.
Option D: AWS SQS
- No existing AWS infrastructure.
- Transport-level auth uses IAM, not JWT.
userIdmust be carried separately in the message body with its own verification chain. - Estimated effort: 7 – 8.5 days.
Authentication design
The JWT travels in the AMQP message header, mirroring the HTTP pattern:
Publisher sets the header on every message:
properties.Headers = new Dictionary<string, object>
{
["Authorization"] = $"Bearer {jwtToken}"
};
Consumer (SimopsConnect) reads and validates it using the extracted JwtValidator service (refactored from HttpService.ValidateTokenAsync):
string? authHeader = Encoding.UTF8.GetString((byte[]) headers["Authorization"]);
JwtSecurityToken? token = await _jwtValidator.ValidateAsync(authHeader);
if (token is null) {
channel.BasicNack(tag, multiple: false, requeue: false); // dead-letter
} else {
channel.BasicAck(tag, multiple: false);
}
Invalid JWT results in BasicNack(requeue: false): the message is dead-lettered, equivalent to a 401 response. There is no synchronous reply channel. Commands start/stop are inherently fire-and-forget.
JWKS caching must be added: the current implementation fetches signing keys on every validation call, which is acceptable for low-frequency HTTP but not for a message consumer. Keys should be cached and refreshed on a ~1-hour interval or on validation failure.
Consequences
Pros
- Commands are decoupled from HTTP: the app no longer needs to listen on a TCP port for inbound traffic.
- The authentication model is preserved:
userIdis cryptographically verified on every message. - For LavinMQ: lower resource consumption on the host, which shares CPU/RAM with Flight Simulator.
- Zero code change to
RequestsHandlerServiceorStateService: only the transport layer changes.
Cons / Risks
- No synchronous response channel: callers cannot receive a reply inline. The state command must move to WebSocket or a separate reply-queue pattern.
- Dead-lettered messages (bad JWT, malformed body) require operational monitoring of the dead-letter queue.
- JWKS caching introduces a window where a revoked token could still be accepted (mitigated by short token TTL on the issuer side).
- LavinMQ has a smaller community and less documentation than RabbitMQ if edge cases arise.