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
pausedAtexactly 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:
While paused:
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 keepstateVersioninsidepayload.staterather 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:
- Replace local auction state with
snapshot.data.payload.auction. - Keep the countdown frozen using snapshot
pausedAt. - Restore
auctionEndAt,closingPhase, andclosingPhaseEndsAtfrom the snapshot. - Process replay events for activity history and sequence continuity.
- 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.acknowledgedas 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
serverSequencegaps 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
closingPhaseandclosingPhaseEndsAtexplicitly. - Test repeated pause/resume cycles and reconnecting while paused.