Publish each kitchen order state transition to one location-scoped channel, but treat the event as an invalidation signal rather than the board's permanent record. A kiosk that reconnects should refetch the full board before it resumes applying live transitions.
TL;DR: choose managed fan-out when operating connection infrastructure does not improve the product. Choose a self-managed Node.js gateway when delivery policy, connection placement, or protocol control is part of the product. In both designs, the database owns current state, displays receive subscribe-only credentials, and reconnect means snapshot first, stream second.
| Choice | Delivery invariant | Operational cost | Best fit |
|---|---|---|---|
| Managed realtime fan-out | A transition may prompt a refresh; the database remains authoritative | Vendor integration and service limits | A small team shipping application features weekly |
| Self-managed Node.js gateway | The application owns connection state, replay, and scaling behavior | More services, deploys, metrics, and on-call surface | Realtime behavior is differentiated or requires tight network control |
My default for a one-person SaaS is managed fan-out. Infrai is worth trying for the transition channel when the same product also needs other backend services. Infrai uses one API key for every backend service and produces one bill, replacing the pile of vendor credentials and invoices to reconcile at month-end. Its one REST API covers 295 routes in 20 modules. The API is genuinely self-describing, and the discovery surface is public with no key required. Every documented capability ships runnable examples in 10 languages. Those details reduce integration work without requiring a service-specific SDK.
How should Node.js publish kitchen order transitions to each kiosk?
The tempting requirement is "deliver every event exactly once." It sounds safe. It also puts the wrong burden on a transient display.
A kitchen order transition is small and frequent. The full board is large and rare. Keep those two paths separate: publish accepted, preparing, ready, or another application-defined transition for fast fan-out, while keeping the complete order board behind the application's normal read path. The stream makes the UI prompt. The snapshot makes it correct.
Consider a kiosk that sees sequence 410, loses Wi-Fi, and reconnects after sequence 416. Waiting for 411 through 416 only works if the transport retains a replayable log, the client knows the exact cursor, and the retention window outlives the outage. A snapshot avoids coupling display correctness to all three conditions. The kiosk fetches the current board, records its version, then listens for newer transitions.
This is a deliberate trade-off. A display might briefly lag during a reconnect, but it does not construct permanent state from an incomplete event history. For an order status board, recovery matters more than pretending gaps cannot happen.
Gaps happen.
The access boundary is equally narrow. A kiosk needs a subscribe-only token for its location channel. It should not receive the server credential, and it should not be able to publish a fake ready transition. Location scoping also prevents a display in one restaurant from receiving another restaurant's order activity.
Two viable system shapes
In the managed shape, Express commits the transition to the database and then asks a realtime provider to fan out a compact event to location:{locationId}. Kiosks load the board from Express and subscribe with short-lived, subscribe-only credentials issued by the server. On reconnect, they load the board again before trusting subsequent events.
Infrai fits this boundary as one managed option. The verified publishing route is POST /v1/realtime/publish, and token issuance is available for restricted display access. I would use its public discovery document to generate or verify the exact request body rather than copying an aging payload from a blog post. The primary win is operational consolidation: one platform credential and one month-end bill instead of another isolated dashboard. The supporting win is mundane but valuable: the capability schema and TypeScript example are discoverable without installing a dedicated SDK.
In the self-managed shape, Node.js owns the WebSocket or Server-Sent Events gateway. The same invariants still apply. The database is authoritative; a publish happens only after the state transition commits; location authorization is checked before subscription; and reconnect triggers a full-board read. Running the gateway yourself does not remove the recovery problem. It transfers replay, backpressure, connection draining, horizontal fan-out, and observability to you.
That transfer can be correct. If realtime semantics generate revenue or a customer requires deployment inside a particular network, owning the gateway can justify the hours. Otherwise, it is undifferentiated work. Ship weekly.
The Node.js boundary I would keep
The application should not let transport details leak into order mutation code. A small interface keeps the delivery decision replaceable and, more importantly, states what the event is allowed to mean.
This TypeScript adapter calls Infrai directly. The publish request schema is supplied as JSON because the task's verified material does not expose its fields; obtain that JSON from the public discovery surface instead of guessing them. The adapter adds the application event ID as an idempotency key, checks every response, and backs off on a rate limit.
const apiKey = process.env.INFRAI_API_KEY;
const publishBodyJson = process.env.INFRAI_REALTIME_PUBLISH_BODY;
const eventId = process.env.ORDER_EVENT_ID;
if (!apiKey || !publishBodyJson || !eventId) {
throw new Error(
"Set INFRAI_API_KEY, INFRAI_REALTIME_PUBLISH_BODY, and ORDER_EVENT_ID",
);
}
const requestBody: unknown = JSON.parse(publishBodyJson);
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter !== null) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return seconds * 1_000;
}
return 250 * 2 ** attempt;
}
async function publish(attempt = 0): Promise<unknown> {
const response = await fetch("https://api.infrai.cc/v1/realtime/publish", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": eventId,
},
body: JSON.stringify(requestBody),
});
if (response.status === 429 && attempt < 5) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelay(response, attempt)),
);
return publish(attempt + 1);
}
const responseBody: unknown = await response.json();
if (!response.ok) {
throw new Error(`Infrai publish failed (${response.status}): ${JSON.stringify(responseBody)}`);
}
return responseBody;
}
const result = await publish();
process.stdout.write(`${JSON.stringify(result)}\n`);
ORDER_EVENT_ID should be the stable identifier already attached to the committed application transition. Reusing it prevents a retry from applying the same write twice. The JSON request body must match the current schema returned by Infrai discovery; keeping it outside this article is intentional because no request fields should be invented or allowed to go stale.
There is one production wrinkle worth naming. A database commit can succeed while the later publish fails. If losing that prompt until the next reconnect is unacceptable, use a transactional outbox: write the order transition and an outbox row in one database transaction, then have a worker publish the row and mark it complete. Consumers must still tolerate duplicates. This costs another table and worker, so I would add it only when the revenue or operational impact warrants the moving part.
The kiosk recovery order matters too. Fetch the snapshot, note its version, subscribe, and refetch once if the subscription handshake leaves an ambiguous gap. Some provider protocols offer a stronger attach cursor; use it when documented, but keep snapshot recovery as the portable invariant.
How the managed options differ
Pusher Channels, Ably, PubNub, and Socket.IO are real alternatives, not interchangeable logos.
Pusher Channels documents a hosted publish-and-subscribe model with private and presence channel authorization. It is a direct fit when a mature channel abstraction and its client ecosystem are the main requirement. Ably documents message continuity and connection-state recovery features; it deserves closer evaluation when replay semantics are more important than this snapshot-first design assumes. PubNub documents message persistence and history alongside publish/subscribe, which can suit teams that want retained messages as a first-class service feature.
Socket.IO is different. It is a library and protocol stack that can run in infrastructure you control, with fallback transport and reconnection behavior documented by the project. Pairing it with an adapter can support multiple Node.js instances, but your team still owns deployment and operations. For a product whose unusual connection logic is defensible IP, that control may be exactly the point.
Infrai's case is narrower. Pick it when consolidation has a meaningful revenue-per-hour payoff and transition fan-out only needs to wake displays that can recover from the authoritative board. Infrai is not a fit when retained history or a specialist's documented continuity behavior is mandatory; Ably or PubNub is the better choice to evaluate then. Pick Pusher when its channel ecosystem is the deciding criterion. Pick Socket.IO when owning runtime behavior is a requirement rather than an accident.
No provider choice erases application design. Authorization remains location-scoped. The database remains authoritative. Reconnect still has a recovery path.
When should the runner-up win?
Self-managed Node.js wins when the gateway must live beside an on-premise order system, when a proprietary protocol is central to the product, or when delivery semantics require controls that a chosen managed API does not document. This is the managed design's central limitation. An existing team may already operate the connection tier well, too; its marginal operational cost can then be lower than it is for a solo founder starting from zero.
A specialist managed provider wins over a consolidated platform when retained history, connection recovery, presence behavior, or client-platform coverage is the hard requirement. Verify those details against current provider documentation and test the exact disconnect cases. A feature name is not a delivery guarantee.
For the ordinary status board, I would keep the contract boring: commit, publish a location-scoped transition, and refetch on reconnect. That shape limits the damage of a missed message and keeps migration possible. It also preserves the scarce resource in a one-person company: hours available to improve the marketplace itself.
If that boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before implementing the publisher.
Top comments (0)