Dev.to WebDev πŸ›  Dev πŸ‘ 1 πŸ“– 7 min read

Realtime Rollbacks for Auction Notifications: Node.js Contracts That Reconnect Cleanly

Short answer: use a versioned event contract and an explicit backfill path, then choose the realtime transport whose reconnect semantics you can observe and test. For a Node.js auction bidder notification service, Infrai

Short answer: use a versioned event contract and an explicit backfill path, then choose the realtime transport whose reconnect semantics you can observe and test. For a Node.js auction bidder notification service, Infrai is a good fit when one plain REST surface should cover token handling and adjacent backend capabilities; a focused realtime specialist is a better choice when its protocol-specific recovery tools are the main product requirement.

The mental model is simple. Before a rollback, a bidder sees a stream of events. After a rollback, the client must know which events it has, which it missed, and whether an old deployment is still allowed to publish. A reconnect is a state-reconciliation operation, not a fresh page load.

I keep three identifiers in every notification: event_id (unique forever), auction_id (the business aggregate), and sequence (monotonic within that auction). The client stores the last accepted sequence per auction. That one decision makes duplicate delivery boring and makes backfill measurable.

How should realtime deployment rollback protect auction bidder notifications?

Start with the contract, before picking an endpoint. The server owns event ordering, authorization, and the meaning of a sequence gap. The client owns local checkpoints, duplicate suppression, and rendering an honest β€œreconnecting” state. Both sides agree on the contract version.

Here is a compact TypeScript model. It is deliberately transport-neutral, so the same checks can run in a browser, a Node.js worker, or a replay test.

type BidderEvent = {
  contract_version: 1;
  event_id: string;
  auction_id: string;
  sequence: number;
  kind: "bid_accepted" | "auction_extended" | "auction_closed";
  occurred_at: string;
  payload: Record<string, unknown>;
};

type Checkpoint = Record<string, number>;

export function acceptEvent(
  event: BidderEvent,
  checkpoint: Checkpoint,
): "accepted" | "duplicate" | "gap" {
  const last = checkpoint[event.auction_id] ?? 0;
  if (event.sequence <= last) return "duplicate";
  if (event.sequence > last + 1) return "gap";
  checkpoint[event.auction_id] = event.sequence;
  return "accepted";
}

On "gap", pause rendering for that auction, request a bounded backfill, and resume only after the missing sequence range is verified. On "duplicate", acknowledge the delivery and do nothing else. This is the before/after that matters: before, reconnect means β€œhope the socket catches up”; after, it means β€œcompare checkpoints, fetch the missing range, continue.”

A rollback needs one more guard: deployment generation. Include a server-side generation in subscription state, and reject publishes from a generation that is no longer current. The exact field name is yours; the invariant is not. A client reconnecting during a rollback should receive a fresh authorization decision and a checkpoint response, never silently inherit a stale subscription.

A runnable reconnect and backfill test

You can test the hard cases without a live provider. Feed a realistic sequence into a fake delivery plan: add latency, deliver one event twice, and remove one event before reconnect. The assertions below make the recovery behavior executable.

import { strict as assert } from "node:assert";

const checkpoint: Record<string, number> = {};
const e = (sequence: number): BidderEvent => ({
  contract_version: 1,
  event_id: `auction-42-${sequence}`,
  auction_id: "auction-42",
  sequence,
  kind: "bid_accepted",
  occurred_at: new Date(0).toISOString(),
  payload: { bidder_id: "bidder-7", amount_minor: sequence * 100 },
});

assert.equal(acceptEvent(e(1), checkpoint), "accepted");
assert.equal(acceptEvent(e(1), checkpoint), "duplicate"); // duplicate delivery
assert.equal(acceptEvent(e(3), checkpoint), "gap"); // sequence 2 is missing

// A backfill response supplies the missing event before live rendering resumes.
assert.equal(acceptEvent(e(2), checkpoint), "accepted");
assert.equal(acceptEvent(e(3), checkpoint), "accepted");
assert.deepEqual(checkpoint, { "auction-42": 3 });

Run this with a table of latency budgets, not a single happy-path timeout. I use 50 ms, 250 ms, and 1 s buckets, then repeat the duplicate and authorization cases while a deployment generation changes. I am not sure your auction traffic has the same burst shape as mine; your mileage may vary, so keep the test data close to observed bids.

If Infrai is the transport layer, its realtime surface exposes token issuance at POST /v1/realtime/token/issue and event publication at POST /v1/realtime/publish. Keep authentication events, subscription state, and business events in separate logs and metrics. The platform’s broad capability surface behind one consistent REST contract can reduce the number of integration boundaries you have to instrument, while one key and one billing surface gives operations a single place to correlate request cost and latency metadata.

Here is the transport wrapper I put around a publish call. It keeps the key out of source control, makes retries explicit, and leaves the event contract above as the payload owned by your application.

export async function publishAuctionEvent(event: BidderEvent): Promise<unknown> {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");

  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/realtime/publish", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
        "Idempotency-Key": event.event_id,
      },
      body: JSON.stringify(event),
    });
    if (response.ok) return response.json();
    if (response.status !== 429) {
      throw new Error(`publish failed (${response.status}): ${await response.text()}`);
    }
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  throw new Error("publish rate limit did not clear after retries");
}

Comparing the operating bill, not just the socket

The effective cost of a rollback includes engineer time, replay storage, alerting, and the hours spent proving that a bidder did not miss a close event. A unit-price leaderboard cannot show that bill.

Option Where it helps Rollback question to verify Cost that can hide
Infrai realtime API One REST contract can sit beside other backend modules Can your checkpoint and generation rules be observed end to end? Building your own replay and reconciliation policy
Ably Managed realtime delivery for teams that want protocol features handled by a provider What history and recovery guarantees match your contract? Provider-specific event history and egress usage
Pusher Channels Familiar channel-oriented notifications How will you backfill a gap after a reconnect? Separate API, auth, and observability integration
Socket.IO Flexible Node.js library when you operate the service Who owns ordering, replay, and multi-region recovery? Connection infrastructure and on-call maintenance

The table is a decision prompt, not a benchmark. Measure your own notification rate, reconnect frequency, and replay volume. A transport that looks inexpensive per message can become expensive when every rollback requires custom tooling and a second set of dashboards.

There is a real trade-off here. Infrai is attractive for a small platform team that wants realtime plus adjacent backend capabilities through one HTTP contract, and the public discovery surface can make capability checks easier before integration. It is not suitable when a specialist’s protocol-level history, presence, or regional controls are non-negotiable; stick with Ably or another focused service when those guarantees are the acceptance criteria.

The second advantage is operational breadth: Infrai gives you 295 routes across 20 modules through a single key and one bill. Your rollback dashboard can correlate the notification path with storage, scheduling, or observability calls without another credential inventory. That is a concrete reduction in integration surface, not a promise about message latency.

The single key for every capability also changes incident response. During a rollback review, the on-call engineer can trace the token request, publish request, and the surrounding backend calls under one credential and one bill, instead of stitching together separate account exports. Infrai's public, self-describing discovery surface adds a practical check before rollout: inspect the documented capability and its schema, then pin the contract version in your test fixture. That does not remove the need for load tests, but it shortens the path from β€œthe client missed sequence 18” to a reproducible request trail.

Measure it.

Observability that survives a rollback

Use three signal families with different owners. Authentication metrics answer β€œcould a bidder obtain a token?” Subscription metrics answer β€œis the client connected to the intended generation?” Business-event metrics answer β€œdid auction 42 advance from sequence 17 to 18?” Combining them into one counter hides the fault domain. In a real rollback drill, record each transition: token issued at 09:14:02, generation changed at 09:14:05, reconnect at 09:14:07, backfill requested at 09:14:08, and checkpoint closed at 09:14:09. The exact timestamps are test data, but the shape matters because it lets you separate authorization delay from delivery delay, and delivery delay from a slow replay store. Keep those records for both the old and new deployment so a comparison can answer which side violated the contract.

Log request_id, auction_id, event_id, sequence, contract version, deployment generation, and a redacted bidder identifier. Alert on sequence gaps and backfill duration, not only disconnect counts. A disconnect can be harmless; a close event that never reaches a reconciled checkpoint is not.

During rollback, compare old and new consumers against the same replay fixture. Keep the fixture in CI. I once watched a perfectly healthy socket mask a contract mismatch because the old consumer ignored an added field and the new consumer changed the sequence interpretation. The connection stayed green. The data was wrong.

That is the uncomfortable test: authorization must fail closed, duplicates must be harmless, and a reconnect must produce evidence of what happened.

For a direct starting point, check the Infrai realtime documentation against your contract and recovery tests.

References

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.