Skip to content

Auction Pause/Resume Realtime — Frontend Integration Guide

This guide covers the realtime contract for pausing and resuming an auction, including timer behavior, persisted event sequencing, gap recovery, and paused reconnects.

Audience: web and mobile engineers consuming the auction WebSocket API.

Related docs:


1. Contract summary

Pause and resume are WebSocket staff commands:

{
  "id": "pause-001",
  "type": "auction.pause",
  "data": {
    "auctionId": "auction-id",
    "reason": "Network interruption"
  }
}
{
  "id": "resume-001",
  "type": "auction.resume",
  "data": {
    "auctionId": "auction-id",
    "reason": "Connection restored"
  }
}

The command acknowledgement only confirms that processing was accepted. Update auction state when the authoritative auction.state.changed event arrives.

Pause and resume events are persisted in the auction activity stream. Each has an event id and a monotonically increasing serverSequence, so missed events can be detected and replayed after reconnect.

No database or request-contract migration is required on the frontend.


2. Live event contract

The examples below abbreviate payload.state to the fields relevant to pause and resume. The actual event contains the complete lifecycle state documented in auction-frontend-integration.md.

2.1 Paused

{
  "id": "evt-paused-42",
  "type": "auction.state.changed",
  "version": "1.0",
  "timestamp": "2026-09-05T10:10:00.000Z",
  "source": "auction-service",
  "data": {
    "auctionId": "auction-id",
    "cycleId": "cycle-id",
    "stateVersion": 12,
    "serverSequence": 42,
    "payload": {
      "transition": "paused",
      "state": {
        "status": "PAUSED",
        "stateVersion": 12,
        "pausedAt": "2026-09-05T10:10:00.000Z",
        "auctionEndAt": "2026-09-05T10:30:00.000Z",
        "closingPhase": "SECOND_CALL",
        "closingPhaseEndsAt": "2026-09-05T10:30:00.000Z"
      }
    }
  }
}

Apply it as follows:

  • Set status to PAUSED.
  • Store pausedAt exactly as received.
  • Replace local deadline fields with the payload values.
  • Stop the visible countdown at the remaining time calculated at pausedAt.
  • Disable bid submission.

2.2 Resumed

{
  "id": "evt-resumed-43",
  "type": "auction.state.changed",
  "version": "1.0",
  "timestamp": "2026-09-05T10:15:00.000Z",
  "source": "auction-service",
  "data": {
    "auctionId": "auction-id",
    "cycleId": "cycle-id",
    "stateVersion": 13,
    "serverSequence": 43,
    "payload": {
      "transition": "resumed",
      "state": {
        "status": "LIVE",
        "stateVersion": 13,
        "pausedAt": null,
        "auctionEndAt": "2026-09-05T10:35:00.000Z",
        "closingPhase": "SECOND_CALL",
        "closingPhaseEndsAt": "2026-09-05T10:35:00.000Z"
      }
    }
  }
}

Apply it as follows:

  • Set status to LIVE.
  • Clear local pausedAt.
  • Replace local deadline fields with the shifted values from the payload.
  • Restart the countdown against the new server deadline.
  • Re-enable bidding only when all other eligibility checks also pass.

Do not add the pause duration on the client. The server has already shifted the deadline. Repeated pauses are therefore safe: every event replaces the current deadline instead of incrementing it locally.


3. Payload fields

type AuctionClosingPhase = 'FIRST_CALL' | 'SECOND_CALL' | 'THIRD_CALL';

interface AuctionPauseResumeState {
  readonly status: 'LIVE' | 'PAUSED';
  readonly stateVersion: number;
  readonly pausedAt: string | null;
  readonly auctionEndAt: string | null;
  readonly closingPhase: AuctionClosingPhase | null;
  readonly closingPhaseEndsAt: string | null;
}

interface AuctionPauseResumePayload {
  readonly transition: 'paused' | 'resumed';
  readonly state: AuctionPauseResumeState; // complete lifecycle state in reality
}
Field Meaning
pausedAt Exact server timestamp persisted when the auction entered PAUSED.
event timestamp Server occurrence time; for resumed, use this only for audit/display.
auctionEndAt Authoritative duration deadline, or active call deadline in call mode.
closingPhase Active call phase; null when no call phase is active.
closingPhaseEndsAt Authoritative active call deadline; null outside an active call phase.
stateVersion Ordering/version guard for the mutable auction state.
serverSequence Ordering and gap-detection cursor for persisted activity events.

All timestamps are ISO-8601 UTC strings. Treat nullable fields as explicitly cleared when the server sends null.


4. Timer behavior

4.1 Duration mode

auctionEndAt is authoritative. While live:

remaining = auctionEndAt - current server-adjusted time

While paused:

remaining = auctionEndAt - pausedAt

On resume, the server adds the elapsed pause duration and sends a new auctionEndAt. Replace the old value.

4.2 Call mode

Render closingPhase and count down to closingPhaseEndsAt. On pause, freeze that countdown. On resume, replace closingPhaseEndsAt with the shifted value from the event.

auctionEndAt may equal the active closingPhaseEndsAt in call mode. Prefer closingPhaseEndsAt for the call-phase UI and keep auctionEndAt as the general effective deadline.

4.3 Countdown helper

interface AuctionClock {
  readonly status: string;
  readonly pausedAt: string | null;
  readonly auctionEndAt: string | null;
  readonly closingPhaseEndsAt: string | null;
}

export function remainingAuctionMs(
  clock: AuctionClock,
  nowMs = Date.now()
): number | null {
  const deadline = clock.closingPhaseEndsAt ?? clock.auctionEndAt;
  if (deadline === null) return null;

  const referenceMs =
    clock.status === 'PAUSED' && clock.pausedAt !== null
      ? Date.parse(clock.pausedAt)
      : nowMs;

  return Math.max(0, Date.parse(deadline) - referenceMs);
}

Use the app's existing server-clock offset, if available, instead of raw Date.now() for a live countdown.


5. Applying live events

interface AuctionRealtimeState {
  readonly status: string;
  readonly stateVersion: number;
  readonly lastSequence: number;
  readonly pausedAt: string | null;
  readonly auctionEndAt: string | null;
  readonly closingPhase: AuctionClosingPhase | null;
  readonly closingPhaseEndsAt: string | null;
}

interface StateChangedFrame {
  readonly id: string;
  readonly type: 'auction.state.changed';
  readonly data: {
    readonly stateVersion: number;
    readonly serverSequence?: number;
    readonly payload:
      | AuctionPauseResumePayload
      | {
          readonly transition: string;
          readonly state: AuctionPauseResumeState;
        };
  };
}

export function applyPauseResumeEvent(
  state: AuctionRealtimeState,
  frame: StateChangedFrame
): AuctionRealtimeState {
  const { stateVersion, serverSequence, payload } = frame.data;

  if (stateVersion <= state.stateVersion) return state;

  const nextSequence = serverSequence ?? state.lastSequence;

  if (payload.transition === 'paused') {
    return {
      ...state,
      status: payload.state.status,
      stateVersion,
      lastSequence: nextSequence,
      pausedAt: payload.state.pausedAt,
      auctionEndAt: payload.state.auctionEndAt,
      closingPhase: payload.state.closingPhase,
      closingPhaseEndsAt: payload.state.closingPhaseEndsAt
    };
  }

  if (payload.transition === 'resumed') {
    return {
      ...state,
      status: 'LIVE',
      stateVersion,
      lastSequence: nextSequence,
      pausedAt: null,
      auctionEndAt: payload.state.auctionEndAt,
      closingPhase: payload.state.closingPhase,
      closingPhaseEndsAt: payload.state.closingPhaseEndsAt
    };
  }

  return { ...state, stateVersion, lastSequence: nextSequence };
}

Deduplicate persisted events by event id. Use stateVersion to prevent stale live state from overwriting newer state.


6. Gap detection and replay

Track the highest durable data.serverSequence applied for the auction. The visible sequence may be sparse because replay is filtered by role, event scope, and target user. Reconnect or resubscribe after transport loss or inconsistent state using the last applied high-water value:

{
  "id": "subscribe-replay-001",
  "type": "auction.subscribe",
  "data": {
    "auctionId": "auction-id",
    "enrolledSubscriberId": "enrollment-id",
    "lastSequence": 41
  }
}

Staff clients omit enrolledSubscriberId.

The server returns an auction.state.snapshot. Persisted events use this shape inside the snapshot:

{
  "type": "auction.state.snapshot",
  "data": {
    "auctionId": "auction-id",
    "cycleId": "cycle-id",
    "payload": {
      "auction": {
        "auctionStatus": "PAUSED",
        "pausedAt": "2026-09-05T10:10:00.000Z",
        "auctionEndAt": "2026-09-05T10:30:00.000Z",
        "closingPhase": "SECOND_CALL",
        "closingPhaseEndsAt": "2026-09-05T10:30:00.000Z"
      },
      "lastSequence": 43,
      "replay": {
        "lastSequence": 43,
        "events": [
          {
            "type": "auction.state.changed",
            "id": "evt-paused-42",
            "serverSequence": 42,
            "payload": {
              "transition": "paused",
              "state": {
                "status": "PAUSED",
                "stateVersion": 12,
                "pausedAt": "2026-09-05T10:10:00.000Z",
                "auctionEndAt": "2026-09-05T10:30:00.000Z",
                "closingPhase": "SECOND_CALL",
                "closingPhaseEndsAt": "2026-09-05T10:30:00.000Z"
              }
            }
          }
        ]
      }
    }
  }
}

Live and replay deliveries now use the same lifecycle payload:

  • Live event: transition payload is frame.data.payload.
  • Replay event: transition payload is snapshot.data.payload.replay.events[n].payload.
  • Both carry the complete post-transition state at payload.state.
  • Both carry serverSequence; replay entries keep stateVersion inside payload.state rather than at their event root.

Use snapshot.data.payload.auction as the authoritative current state. Consume replay.events in ascending serverSequence order to fill activity/history, deduplicate events, and advance the sequence cursor. Do not apply old replay events over the current snapshot and regress its status or deadlines.


7. Paused reconnect behavior

When reconnecting during a pause:

  1. Replace local auction state with snapshot.data.payload.auction.
  2. Keep the countdown frozen using snapshot pausedAt.
  3. Restore auctionEndAt, closingPhase, and closingPhaseEndsAt from the snapshot.
  4. Process replay events for activity history and sequence continuity.
  5. Begin applying new live events after the snapshot/replay boundary.

Do not infer a resume because the socket reconnected. Only a snapshot with auctionStatus: "LIVE" or a newer live transition: "resumed" unfreezes the clock.


8. Frontend checklist

  • Store stateVersion, lastSequence, and processed event IDs per auction.
  • Treat command.acknowledged as acceptance, not completion.
  • Apply pause/resume state from auction.state.changed.
  • Replace server deadlines; never add pause duration locally.
  • Freeze duration and call-mode timers while status is PAUSED.
  • Disable bidding while paused.
  • Do not treat numeric serverSequence gaps as loss; scoped events are filtered.
  • Resubscribe with the last applied/reported high-water cursor after real transport loss or inconsistent state.
  • Use the reconnect snapshot as authoritative current state.
  • Deduplicate replay/live overlap by event id.
  • Handle nullable closingPhase and closingPhaseEndsAt explicitly.
  • Test repeated pause/resume cycles and reconnecting while paused.