Skip to main content

Real-Time Operations

The tee-sheet admin dashboard uses WebSocket connections for real-time updates, enabling live slot status changes, check-in notifications, pace alerts, and waitlist updates without page refresh.

Architecture

┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│ Admin Client │────▶│ TeeSheetGateway │◀────│ Redis Pub/Sub │
│ (React + WS) │◀────│ (NestJS WS) │────▶│ │
└─────────────────┘ └──────────────────┘ └─────────────────┘


┌──────────────────┐
│ Domain Services │
│ (publish events)│
└──────────────────┘

Source: libs/tee-time-services/src/lib/gateway/tee-sheet.gateway.ts

WebSocket Gateway

Connection Lifecycle

  1. Connect: Client establishes WebSocket connection
  2. Authenticate: Client sends JWT token via auth message
  3. Subscribe: Client joins rooms for courses/dates of interest
  4. Receive: Gateway pushes events to subscribed clients
  5. Disconnect: Cleanup room memberships

Authentication

// Client sends after connection
ws.send(JSON.stringify({
event: 'auth',
data: { token: 'eyJhbG...' }
}));

// Gateway validates JWT and extracts:
// - tenantId
// - clubIds (for authorization)
// - playerId (optional)

Room Subscriptions

Clients subscribe to rooms to receive targeted updates:

Room PatternUse CaseExample
club:{clubId}All updates for a clubclub:123
course:{courseId}:{date}Slot updates for specific daycourse:456:2025-12-15
booking:{bookingId}Track specific bookingbooking:abc-123
// Subscribe to a course/date
ws.send(JSON.stringify({
event: 'subscribe',
data: { room: 'course:456:2025-12-15' }
}));

// Unsubscribe
ws.send(JSON.stringify({
event: 'unsubscribe',
data: { room: 'course:456:2025-12-15' }
}));

Event Types

Slot Lifecycle Events

EventWhenUI Action
slot.availableBooking cancelled, hold expiredUpdate tile to green
slot.heldSomeone started bookingShow hold indicator
slot.bookedBooking confirmedUpdate tile with player info
slot.blockedAdmin blocked slotShow blocked state
slot.releasedHold manually releasedUpdate tile to available

Booking Events

EventWhenUI Action
booking.createdNew bookingAdd to grid, update stats
booking.cancelledCancellationRemove from grid
booking.updatedDetails changedRefresh booking info

Check-In & Pace Events

EventWhenUI Action
booking.checked-inPlayer checks inShow check mark on tile
booking.check-in-undoneCheck-in reversedRemove check mark
booking.tee-offGroup tees offUpdate starter view
booking.pace-updatedHole completionUpdate pace indicator
pace.alertSlow play detectedShow warning banner

Course Events

EventWhenUI Action
tee-sheet.offlineCourse taken offlineShow offline banner
tee-sheet.onlineCourse back onlineRemove banner
block.addedNew block createdAdd to grid
block.removedBlock deletedRemove from grid
course.status.changedStatus transitionUpdate status indicator

Waitlist Events

EventWhenUI Action
waitlist.joinedPlayer joins queueUpdate waitlist count
waitlist.leftPlayer leaves queueUpdate waitlist count
waitlist.offer-createdOffer sentShow in waitlist panel
waitlist.offer-acceptedPlayer acceptedConvert to booking
waitlist.offer-declinedPlayer declinedCascade to next
waitlist.offer-expiredTimeoutCascade to next

Event Payload

All events follow this structure:

interface TeeSheetEvent {
type: string; // Event type
clubId: string; // Always present
courseId?: string; // For course-specific events
date?: string; // YYYY-MM-DD format
bookingId?: string; // For booking events
slotId?: string; // For slot events
teeTimeId?: string; // Tee time reference
tenantId: string; // Tenant identifier
timestamp: string; // ISO 8601 timestamp
data: { // Event-specific payload
status?: string;
priceCents?: number;
checkedInAt?: string;
// ... varies by event type
};
}

Redis Fan-Out

The gateway uses Redis pub/sub for multi-instance deployments:

Instance A (publish) ──▶ Redis Channel ──▶ Instance B (broadcast)

└──────────▶ Instance C (broadcast)

Channel: tee-sheet:events

Publishing Events

Services publish via the gateway:

// In a service
await this.gateway.publish({
type: 'slot.booked',
clubId: '123',
courseId: '456',
date: '2025-12-15',
slotId: 'slot-789',
tenantId: 'tenant-1',
timestamp: new Date().toISOString(),
data: {
status: 'BOOKED',
bookingId: 'booking-abc',
payer: { firstName: 'John', lastName: 'Smith' }
}
});

Frontend Integration

TeeSheetDashboard WebSocket Hook

// In TeeSheetDashboard.tsx
useEffect(() => {
const ws = new WebSocket(WS_URL);

ws.onopen = () => {
ws.send(JSON.stringify({ event: 'auth', data: { token } }));
ws.send(JSON.stringify({
event: 'subscribe',
data: { room: `course:${courseId}:${dateStr}` }
}));
};

ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'slot.booked':
updateSlotInGrid(msg.slotId, msg.data);
break;
case 'booking.checked-in':
markSlotCheckedIn(msg.bookingId);
break;
case 'pace.alert':
showPaceAlert(msg.data);
break;
// ... handle other events
}
};

return () => ws.close();
}, [courseId, dateStr]);

Pace Alert Service

The PaceAlertService monitors booking pace and emits alerts:

Source: libs/tee-time-services/src/lib/gateway/pace-alert.service.ts

Alert Levels

LevelConditionAction
info5-10 min behindInformational
warning10-15 min behindYellow indicator
critical15+ min behindRed alert, notify starter

Pace Calculation

// Expected pace based on course config
const expectedMinutes = booking.expectedPaceMinutes || 240; // 4 hours default

// Actual elapsed time
const elapsed = Date.now() - teeOffAt.getTime();
const holesCompleted = booking.lastHoleCompleted || 0;

// Variance
const expectedForHoles = (holesCompleted / 18) * expectedMinutes;
const variance = (elapsed / 60000) - expectedForHoles;

if (variance > 15) {
// Emit critical pace alert
}

Configuration

Env VarDescriptionDefault
WS_PORTWebSocket port3001
REDIS_URLRedis for pub/subredis://localhost:6379
WS_HEARTBEAT_INTERVALPing interval (ms)30000
WS_AUTH_TIMEOUTAuth timeout (ms)10000

Metrics

MetricDescription
teesheet_ws_connections_totalTotal connections
teesheet_ws_connections_activeCurrent active connections
teesheet_ws_messages_sent_totalMessages sent by type
teesheet_ws_auth_failures_totalAuthentication failures
teesheet_ws_room_subscriptionsActive room subscriptions

Troubleshooting

Connection Issues

# Check WebSocket port is open
nc -zv teetime-api.example.com 3001

# Check Redis pub/sub
redis-cli subscribe tee-sheet:events

Missing Events

  1. Verify client is subscribed to correct room
  2. Check Redis connectivity between instances
  3. Verify JWT token has required claims
  4. Check gateway logs for publish errors

High Latency

  1. Check Redis latency: redis-cli --latency
  2. Review message payload sizes
  3. Check for slow subscribers blocking delivery