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
- Connect: Client establishes WebSocket connection
- Authenticate: Client sends JWT token via
authmessage - Subscribe: Client joins rooms for courses/dates of interest
- Receive: Gateway pushes events to subscribed clients
- 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 Pattern | Use Case | Example |
|---|---|---|
club:{clubId} | All updates for a club | club:123 |
course:{courseId}:{date} | Slot updates for specific day | course:456:2025-12-15 |
booking:{bookingId} | Track specific booking | booking: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
| Event | When | UI Action |
|---|---|---|
slot.available | Booking cancelled, hold expired | Update tile to green |
slot.held | Someone started booking | Show hold indicator |
slot.booked | Booking confirmed | Update tile with player info |
slot.blocked | Admin blocked slot | Show blocked state |
slot.released | Hold manually released | Update tile to available |
Booking Events
| Event | When | UI Action |
|---|---|---|
booking.created | New booking | Add to grid, update stats |
booking.cancelled | Cancellation | Remove from grid |
booking.updated | Details changed | Refresh booking info |
Check-In & Pace Events
| Event | When | UI Action |
|---|---|---|
booking.checked-in | Player checks in | Show check mark on tile |
booking.check-in-undone | Check-in reversed | Remove check mark |
booking.tee-off | Group tees off | Update starter view |
booking.pace-updated | Hole completion | Update pace indicator |
pace.alert | Slow play detected | Show warning banner |
Course Events
| Event | When | UI Action |
|---|---|---|
tee-sheet.offline | Course taken offline | Show offline banner |
tee-sheet.online | Course back online | Remove banner |
block.added | New block created | Add to grid |
block.removed | Block deleted | Remove from grid |
course.status.changed | Status transition | Update status indicator |
Waitlist Events
| Event | When | UI Action |
|---|---|---|
waitlist.joined | Player joins queue | Update waitlist count |
waitlist.left | Player leaves queue | Update waitlist count |
waitlist.offer-created | Offer sent | Show in waitlist panel |
waitlist.offer-accepted | Player accepted | Convert to booking |
waitlist.offer-declined | Player declined | Cascade to next |
waitlist.offer-expired | Timeout | Cascade 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
| Level | Condition | Action |
|---|---|---|
info | 5-10 min behind | Informational |
warning | 10-15 min behind | Yellow indicator |
critical | 15+ min behind | Red 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 Var | Description | Default |
|---|---|---|
WS_PORT | WebSocket port | 3001 |
REDIS_URL | Redis for pub/sub | redis://localhost:6379 |
WS_HEARTBEAT_INTERVAL | Ping interval (ms) | 30000 |
WS_AUTH_TIMEOUT | Auth timeout (ms) | 10000 |
Metrics
| Metric | Description |
|---|---|
teesheet_ws_connections_total | Total connections |
teesheet_ws_connections_active | Current active connections |
teesheet_ws_messages_sent_total | Messages sent by type |
teesheet_ws_auth_failures_total | Authentication failures |
teesheet_ws_room_subscriptions | Active 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
- Verify client is subscribed to correct room
- Check Redis connectivity between instances
- Verify JWT token has required claims
- Check gateway logs for publish errors
High Latency
- Check Redis latency:
redis-cli --latency - Review message payload sizes
- Check for slow subscribers blocking delivery