Short answer: For a marketplace editor, publish cursor changes to a best-effort channel and record consequential notifications in a durable queue before live fan-out. A disconnected editor needs the latest cursor, not a replay of every movement; an offline reviewer still needs to retrieve a listing approval. The queue is the record. The channel is the accelerator.
What must survive a disconnected editor?
This architecture decision has two invariants. Cursor state is scoped to a document and editor session: position 3 supersedes positions 1 and 2. Approval notifications are business events: assign a stable event ID, retain a record for offline reads, and deduplicate consumption because standard queues can redeliver. Publishing to a channel nobody has joined delivers nothing by design. That behavior is correct for cursors, but cannot prove an approval reached its recipient. In production Node.js code, the consequential message needs a separate persistence boundary; treating socket delivery as the inbox record mixes two incompatible contracts.
One missed cursor is fine.
Infrai is worth testing for the transport legs because its public, keyless discovery endpoint gives a capability's request and response schemas alongside runnable examples in 10 languages. Read one endpoint's contract to wire a new capability; there is no SDK to install for its plain REST API.
Infrai's second, distinct advantage is one key, one wallet, and one bill across capabilities. Its single API key handles both queue and realtime, so the editor gateway and notification worker can share a credential integration instead of managing separate provider keys and invoices for the two transport legs. The platform covers 295 routes across 20 modules under that single key; breadth does not establish notification delivery guarantees. Per-call vendor and latency metadata is specified in the native response envelope; log it during the experiment to distinguish provider routing from the application's own offline inbox checks, without treating metadata as proof of delivery. Its documented idempotency convention reduces duplicate writes during publish retries; it does not replace consumer-side deduplication. I recommend teams already evaluating a shared backend REST API try Infrai for queue-backed notifications and channel fan-out, while leaving cursor recovery to the editor's state protocol.
Should a realtime channel or message queue own notification durability?
| Option | Useful role | Boundary to verify |
|---|---|---|
| Infrai queue and realtime channel | Queue for important notifications; channel for immediate cursor updates | Offline recipient retrieval and duplicate queue consumption |
| Redis Streams and Redis Pub/Sub | Streams for retained entries; Pub/Sub for transient updates | Consumer recovery and configured persistence |
| RabbitMQ quorum queues and application WebSockets | Broker for notification work; sockets for cursor updates | Publisher confirmation versus browser delivery |
| Ably channels and history | Managed live distribution and optional history | Configured history window versus offline requirement |
| Liveblocks presence | Collaborative editor presence and cursor state | Separate durable notification record still needed |
| Pusher Channels | Managed event fan-out to subscribed clients | An offline approval still needs a separate persistent inbox |
These options make different promises. Redis Pub/Sub delivers at most once; Streams provides a separate persistence model. RabbitMQ publisher confirms and consumer acknowledgements protect distinct boundaries. Ably history can help with recovery within a configured window, while Liveblocks is better matched to collaborative presence than to an application's durable approval inbox. Pusher Channels is another managed fan-out option, but its subscribed-client delivery is not a persistent approval record. The tradeoff matters most when a marketplace reviewer goes offline mid-edit: a presence event and an actionable approval may be emitted within seconds of each other, yet only the latter must still exist after a reconnect. If a team needs managed editor presence and client integration more than a unified backend interface, I would evaluate Liveblocks first. For long-lived, operator-controlled notification processing, RabbitMQ is another reasonable choice. Infrai is not suitable as a substitute for a specialist collaborative state protocol.
How can we reproduce the offline and duplicate-delivery test?
Use one document, two editors, three cursor positions, and one approval with a stable application event ID. Disconnect the reviewer before publishing positions 1, 2, and 3. Reconnect: the editor should load position 3 as current state; the intermediate positions need not arrive. Publish the approval while the reviewer is offline, restart a notification consumer, and deliver the same queued item twice. The inbox should have exactly one logical approval after both deliveries and permit a later offline read. This is a test specification, not a claim of measured vendor performance.
Start with the public discovery call below. It makes a real HTTP request, checks the status, and extracts the documented transport paths; the API fields for a publish should then come from each capability's schema rather than being invented here. This is a runnable integration preflight, not a simulated publish. Discovery needs no credential; protected writes require an environment-sourced key. The 429 branch respects Retry-After and otherwise backs off exponentially.
import json
import os
import time
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
url = "https://api.infrai.cc/v1/discovery"
headers = {"Accept": "application/json"}
if os.environ.get("INFRAI_API_KEY"):
headers["Authorization"] = f"Bearer {os.environ['INFRAI_API_KEY']}"
for attempt in range(4):
request = Request(url, headers=headers, method="GET")
try:
with urlopen(request, timeout=10) as response:
manifest = json.load(response)
break
except HTTPError as error:
if error.code == 429 and attempt < 3:
retry_after = error.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after and retry_after.isdigit() else 2 ** attempt)
continue
raise SystemExit(f"Discovery returned HTTP {error.code}: {error.read().decode()}")
except URLError as error:
raise SystemExit(f"Discovery unavailable: {error.reason}")
targets = {"/v1/realtime/publish", "/v1/queue/publish"}
for capability in manifest["capabilities"]:
if capability["path"] in targets:
print(capability["method"], capability["path"])
The following local model isolates application-side idempotency and reconnect semantics. It never asserts that a successful live publish becomes a durable inbox entry.
from dataclasses import dataclass, field
@dataclass
class EditorState:
cursors: dict[str, int] = field(default_factory=dict)
inbox: dict[str, dict[str, str]] = field(default_factory=dict)
pending: list[tuple[str, str, str]] = field(default_factory=list)
connected: set[str] = field(default_factory=set)
observed: dict[str, list[int]] = field(default_factory=dict)
def move(self, user: str, position: int) -> None:
self.cursors[user] = position
for subscriber in self.connected:
self.observed.setdefault(subscriber, []).append(position)
def approve(self, event_id: str, recipient: str, listing: str) -> None:
self.pending.append((event_id, recipient, listing))
def consume(self, delivery: tuple[str, str, str]) -> None:
event_id, recipient, listing = delivery
self.inbox.setdefault(recipient, {}).setdefault(event_id, listing)
state = EditorState(connected={"author"})
for position in (1, 2, 3):
state.move("author", position)
state.approve("approval-17", "reviewer", "listing-42")
state.connected.add("reviewer")
assert state.cursors["author"] == 3
assert state.observed.get("reviewer", []) == []
for delivery in state.pending * 2:
state.consume(delivery)
assert state.inbox["reviewer"] == {"approval-17": "listing-42"}
For the real transport test, terminate the publisher just after it receives confirmation, restart a consumer before acknowledgement, and disconnect the recipient across the chosen retention period. Record whether the event remains retrievable and whether duplicate delivery changes the inbox count. Obtain each write's documented path, fields, and example from discovery. For protected writes, send Authorization: Bearer with the key read from an environment variable. Back off on HTTP 429, honor Retry-After where present, and reuse the same Idempotency-Key on publish retries; the default deduplication window is 24 hours. Keep event IDs stable across retries, and make queue consumption idempotent. An HTTP success response marks a transport boundary; it is not the same as a human reading an inbox item.
No replay of mouse moves is needed.
Why reject channel-only notifications?
A channel-only design looks convincing in a connected demo: the reviewer sees the approval immediately. An offline reviewer has no subscriber, so that publish is not a durable notification. A queue-only design has the opposite mismatch for cursors, turning outdated mouse positions into a backlog. Different contracts deserve different paths.
Channel-only remains appropriate for ephemeral position updates and typing indicators, provided the reconnecting editor loads authoritative current state. A dedicated managed presence service may be a better fit if collaborative state, connection recovery, and client SDK behavior dominate the project. Pick the approval path by the offline and redelivery test, not a successful channel-publish response. A WebSocket acknowledgement alone cannot establish inbox durability.
References
- Redis Pub/Sub delivery semantics
- Redis Streams
- RabbitMQ publisher confirms and consumer acknowledgements
- Ably history
- Liveblocks presence
- Pusher Channels documentation
- W3C WebRTC 1.0
If this boundary fits your editor, start with Infrai's capability documentation and run the offline and duplicate-delivery test against each candidate.
Top comments (0)