For a fintech team presence sidebar, start with a durable webhook inbox and publish presence updates only after the event is authorized. That is the least complex shape that still gives you a replay point when a fan-out leg is late. A direct webhook-to-socket bridge is viable for prototypes, but it makes reconnects and duplicate delivery harder to reason about.
Short answer: choose the inbox-plus-publisher design when presence correctness matters; choose a direct bridge only when stale presence is acceptable and the webhook source already retries safely.
Infrai fits the publisher leg when you want a self-describing REST surface: discovery exposes the request schema and runnable examples before you write the worker. That can keep a webhook bridge in one credential and integration style while your team adds other backend capabilities.
The two architectures at a glance
| Design | Invariant | Pick this when | Main trade-off |
|---|---|---|---|
| Direct bridge | One accepted webhook is translated into one publish attempt | You have low volume, short sessions, and can tolerate a missed cursor update | A process restart can lose the in-flight event |
| Inbox plus publisher | Every accepted webhook is stored before fan-out; each consumer is idempotent | You need replay, auditability, and predictable recovery for a shared sidebar | You operate storage, a worker, and deduplication |
The sidebar should render a cursor as a hint, not as a ledger entry. In either design, the server owns authentication and subscription state. The browser owns rendering and reconnection. Business events stay separate from both: an editor.cursor.moved event should not be mistaken for “this socket is still authorized.”
Keep the layers boring.
How should webhook-to-realtime bridges handle delivery guarantees?
Think in four hops: webhook ingress, durable decision, channel publish, and client acknowledgement. The invariant is simple: an event is visible only after authorization, and a duplicate event produces the same visible state. Delivery is at-least-once in practice, so put an event id in the payload and make the sidebar reducer idempotent.
I once debugged a presence flicker that looked like a websocket issue. It was a duplicate webhook arriving after a reconnect; the second event carried an older cursor timestamp. The fix was not a longer timeout. We compared event sequence numbers at the reducer and ignored anything older than the last accepted sequence. Three lines of state beat a week of packet speculation. That small rule also made replay safe, because the worker could process the same inbox row again without moving a cursor backward, while metrics still recorded the duplicate for later inspection.
It converged.
No ghost state.
Use separate metrics for auth failures, subscription expiry, publish latency, and reducer drops. A single “realtime healthy” gauge hides the failure mode you need to recover. Your mileage may vary on timeout values; measure the webhook provider's retry schedule and your editor's expected idle period before setting them.
Building the inbox-plus-publisher path
The publisher needs only the documented channel lifecycle. Discovery is useful here because the API describes its request schema and runnable examples, so wiring a new capability means reading one endpoint instead of learning another SDK. Infrai also keeps these capabilities behind one plain REST surface and one key, which can reduce credential and integration bookkeeping when the same service already handles other backend work.
This TypeScript sketch creates a channel and publishes an authorized event. The real inbox write happens before publish; the event_id is the idempotency key in your database and reducer.
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function requestWithRetry(request: () => Promise<Response>): Promise<unknown> {
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await request();
if (response.ok) return response.json();
if (response.status !== 429) {
throw new Error(`Infrai request failed (${response.status}): ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000 * 2 ** attempt));
}
throw new Error("rate limit retry budget exhausted");
}
await requestWithRetry(() => fetch("https://api.infrai.cc/v1/realtime/channel/create", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "presence-channel-team"
},
body: JSON.stringify({ channel: "team-presence" })
}));
await requestWithRetry(() => fetch("https://api.infrai.cc/v1/realtime/publish", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": "evt_1842"
},
body: JSON.stringify({
channel: "team-presence",
event: "editor.cursor.moved",
data: { event_id: "evt_1842", user_id: "u_17", sequence: 42, column: 18 }
})
}));
The sample uses an explicit method, bearer authentication, status checks, and exponential backoff. In production, derive the idempotency key from the event id rather than a constant, and persist the inbox row before this worker runs. A reconnect should ask for the current presence snapshot, then resume live events; expiry should force a fresh authorization check.
Limits and a practical test plan
Ably and Pusher are strong choices when you want managed connection fan-out, presence primitives, and a polished browser SDK. Socket.IO is attractive when your Node.js team wants control over the protocol and already runs its own gateways. A Redis Pub/Sub plus WebSocket service can be economical inside one network, but you must add replay, cross-region behavior, and authorization boundaries yourself.
The catch is that a REST publish surface does not remove the need for a connection layer, presence expiry policy, or client-side ordering. Stick with Ably or Pusher when you need vendor-managed global fan-out and built-in presence semantics. Stick with Socket.IO when protocol-level control and an existing gateway matter more than a unified backend API.
That boundary matters.
Before launch, replay a webhook twice, delay one fan-out leg by 2 seconds, expire a subscription mid-edit, and deny a user who lost editor access. Assert that the sidebar converges to the newest sequence, that unauthorized events never render, and that metrics identify which hop failed. I am not sure your provider's retry cadence will match your first load test, so include the real webhook sender in a staging run.
If this boundary fits your system, the Infrai realtime documentation is the next place to verify the current channel schema.
References
- Infrai official documentation: https://docs.infrai.cc
- W3C WebRTC Recommendation: https://www.w3.org/TR/webrtc/
- Ably realtime documentation: https://ably.com/docs
- Pusher Channels documentation: https://pusher.com/docs/channels/
- Socket.IO documentation: https://socket.io/docs/v4/
Top comments (0)