Skip to main content

Overview

Rodando Backend uses Socket.io for real-time, bidirectional communication. The application provides separate WebSocket namespaces for different user types:
  • /drivers - Driver availability and location tracking
  • /passengers - Passenger trip updates
  • /admin - Administrative dashboard updates

Architecture

The WebSocket implementation is built with:
  • @nestjs/websockets - NestJS WebSocket support
  • @nestjs/platform-socket.io - Socket.io adapter
  • socket.io - Real-time engine

Gateway Configuration

Driver Availability Gateway

Handles driver location updates, status changes, and trip assignments:
@WebSocketGateway({
  namespace: '/drivers',
  cors: { origin: '*', credentials: true },
  pingTimeout: 20000,
  pingInterval: 25000,
})
export class DriverAvailabilityGateway {}
Configuration:
  • Namespace: /drivers
  • Ping Timeout: 20 seconds
  • Ping Interval: 25 seconds
  • CORS: Enabled with credentials

Passenger Gateway

Handles passenger trip updates and notifications:
@WebSocketGateway({ 
  namespace: '/passengers', 
  cors: true 
})
export class PassengerGateway {}
Configuration:
  • Namespace: /passengers
  • CORS: Enabled

Authentication

All WebSocket connections require JWT authentication:
1

Client sends token

Token can be provided in two ways:
// Option 1: Using auth object
const socket = io('http://localhost:3000/drivers', {
  auth: {
    token: 'your-jwt-access-token'
  }
});

// Option 2: Using Authorization header
const socket = io('http://localhost:3000/drivers', {
  extraHeaders: {
    'Authorization': 'Bearer your-jwt-access-token'
  }
});
2

Server validates token

The gateway extracts and verifies the JWT:
const token = client.handshake.auth?.token || 
  client.handshake.headers?.authorization?.replace(/^Bearer\s+/i, '');

const payload = this.tokenService.verifyAccessToken(token);
3

User and session validation

  • Verifies user exists and is active
  • Checks user type matches the namespace
  • Validates session is not revoked
  • Ensures access token has not expired
4

Connection established

On success, the client joins appropriate rooms and receives a hello event:
socket.on('hello', (data) => {
  console.log('Connected:', data); // { ok: true, nsp: '/drivers' }
});
If authentication fails, the connection is immediately disconnected.

Rooms and Topics

Clients are automatically joined to specific rooms based on their identity:

Driver Rooms

// Individual driver room
await client.join(Rooms.driver(userId)); // "driver:{userId}"

// Session-specific room
await client.join(`session:${sid}`); // "session:{sessionId}"

Passenger Rooms

// Individual passenger room
await client.join(Rooms.passenger(userId)); // "passenger:{userId}"

// Session-specific room
await client.join(`session:${sid}`); // "session:{sessionId}"

Admin Rooms

// Broadcast to all admins monitoring drivers
this.server.to('admin:drivers').emit('driver:availability:update', data);

Driver Events

Client → Server Events

Update Driver Status

socket.emit('driver:status:update', {
  availabilityStatus: 'AVAILABLE' // or 'BUSY', 'OFFLINE'
}, (response) => {
  console.log('Status updated:', response);
});
DTO:
class UpdateDriverStatusDto {
  availabilityStatus: 'AVAILABLE' | 'BUSY' | 'OFFLINE';
}

Update Driver Location

socket.emit('driver:location:ping', {
  latitude: 40.7128,
  longitude: -74.0060
}, (response) => {
  console.log('Location updated:', response);
});
DTO:
class UpdateDriverLocationDto {
  latitude: number;
  longitude: number;
}

Set Current Trip

socket.emit('driver:trip:set', {
  tripId: '123e4567-e89b-12d3-a456-426614174000'
}, (response) => {
  console.log('Trip assigned:', response);
});
DTO:
class UpdateDriverTripDto {
  tripId: string | null; // null to clear trip
}

Server → Client Events

Availability Update

socket.on('driver:availability:update', (data) => {
  console.log('Availability changed:', data);
});
Payload:
{
  driverId: string;
  availabilityStatus: string;
  isAvailableForTrips: boolean;
  availabilityReason: string;
  // ... other driver availability fields
}

Location Update

socket.on('driver:location:update', (data) => {
  console.log('Location:', data);
});
Payload:
{
  driverId: string;
  lastLocation: {
    type: 'Point';
    coordinates: [longitude, latitude];
  };
  lastLocationTimestamp: Date;
}

Trip Update

socket.on('driver:trip:update', (data) => {
  console.log('Trip status:', data);
});
Payload:
{
  driverId: string;
  currentTripId: string | null;
  availabilityReason: string;
  isAvailableForTrips: boolean;
}

Connection Lifecycle

1

Connection

const socket = io('http://localhost:3000/drivers', {
  auth: { token: accessToken }
});

socket.on('connect', () => {
  console.log('Connected:', socket.id);
});
2

Authentication

Server validates JWT and user permissions:
  • Verifies token signature
  • Checks user type and status
  • Validates session
3

Room Assignment

Client automatically joins:
  • User-specific room
  • Session-specific room
4

Active Communication

Client can now:
  • Send events to server
  • Receive broadcasts
  • Get acknowledgments
5

Disconnection

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});
Server logs disconnection with user context.

Testing WebSocket Connections

The project includes test scripts for WebSocket functionality:
# Test driver WebSocket connection
npm run ws:test:da:driver

# Test admin WebSocket connection
npm run ws:test:da:admin

# Test passenger WebSocket connection
npm run ws:test:trip:passenger

# Test full trip flow
npm run ws:test:trip:flow

Client Example

Here’s a complete example of connecting as a driver:
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000/drivers', {
  auth: {
    token: 'your-jwt-access-token'
  },
  transports: ['websocket']
});

// Connection events
socket.on('connect', () => {
  console.log('Connected!');
});

socket.on('hello', (data) => {
  console.log('Handshake:', data);
});

// Update location every 10 seconds
setInterval(() => {
  navigator.geolocation.getCurrentPosition((position) => {
    socket.emit('driver:location:ping', {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude
    }, (ack) => {
      console.log('Location ACK:', ack);
    });
  });
}, 10000);

// Listen for availability updates
socket.on('driver:availability:update', (data) => {
  console.log('Availability updated:', data);
});

// Handle disconnection
socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
});

// Error handling
socket.on('error', (error) => {
  console.error('Socket error:', error);
});

Broadcasting

The server broadcasts updates to relevant clients:
// Broadcast to specific driver
this.server.to(`driver:${driverId}`).emit('driver:availability:update', data);

// Broadcast to admin dashboard
this.server.to('admin:drivers').emit('driver:location:update', data);

// Broadcast to specific session
this.server.to(`session:${sessionId}`).emit('notification', data);

WebSocket Adapter

The application uses a custom Socket.io adapter configured in main.ts:
import { SocketIoAdapter } from './realtime/adapters/socket-io.adapter';

app.useWebSocketAdapter(new SocketIoAdapter(app));
This adapter integrates Socket.io with NestJS and handles CORS configuration.

Best Practices

Keep Tokens Fresh

Use refresh tokens to obtain new access tokens before they expire.

Handle Reconnection

Implement automatic reconnection logic with exponential backoff.

Validate Events

Always validate incoming event data using DTOs and class-validator.

Monitor Connections

Track connection counts and implement rate limiting if needed.

Error Handling

// Client-side error handling
socket.on('connect_error', (error) => {
  console.error('Connection error:', error.message);
  // Attempt to refresh token and reconnect
});

socket.on('exception', (error) => {
  console.error('Server exception:', error);
});

Next Steps

API Reference

Explore REST API endpoints

Configuration

Configure WebSocket settings

Build docs developers (and LLMs) love