> ## Documentation Index
> Fetch the complete documentation index at: https://internal.mechzie.in/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Real-time Events

> Socket.io connection, authentication, rooms, and event reference for live tracking and dispatch

## Overview

MechZie uses **Socket.io** (not raw WebSocket) for all real-time communication. You **must** use the `socket.io-client` package — native WebSocket connections will fail.

<Warning>
  **Do not use raw WebSocket.** Socket.io has its own handshake protocol with rooms, namespaces, and automatic reconnection. Install `socket_io_client` for Flutter or `socket.io-client` for web.
</Warning>

## Connecting

### Flutter (Dart)

```dart theme={null}
import 'package:socket_io_client/socket_io_client.dart' as io;

final socket = io.io(
  'https://mechzie-api-production.run.app',
  io.OptionBuilder()
    .setTransports(['websocket'])
    .disableAutoConnect()
    .setAuth({'token': accessToken})  // JWT access token
    .build(),
);

socket.onConnect((_) => print('Connected: ${socket.id}'));
socket.onDisconnect((_) => print('Disconnected'));
socket.onConnectError((err) => print('Connection error: $err'));

socket.connect();
```

### JavaScript / TypeScript

```typescript theme={null}
import { io } from 'socket.io-client';

const socket = io('https://mechzie-api-production.run.app', {
  transports: ['websocket'],
  auth: {
    token: accessToken,  // JWT access token
  },
});

socket.on('connect', () => console.log('Connected:', socket.id));
socket.on('disconnect', () => console.log('Disconnected'));
socket.on('connect_error', (err) => console.error('Auth failed:', err.message));
```

### Authentication

The server verifies the JWT from `auth.token` on connection. If the token is invalid or expired, the connection is rejected with a `connect_error` event.

**On token refresh**, disconnect and reconnect with the new token:

```dart theme={null}
socket.disconnect();
socket.io.options['auth'] = {'token': newAccessToken};
socket.connect();
```

## Rooms

When connected, the server automatically joins you to rooms based on your identity:

| Room                           | Auto-joined?  | Who                 | Purpose                                           |
| ------------------------------ | ------------- | ------------------- | ------------------------------------------------- |
| `user:{userId}`                | ✅ Always      | All users           | User-scoped events (payment failures, job status) |
| `mechanic:{mechanicProfileId}` | ✅ If mechanic | Verified mechanics  | Dispatch offers (`job:offer`, `job:taken`)        |
| `job:{jobId}`                  | ❌ Manual      | Customer + Mechanic | Live location tracking for a specific job         |

### Joining a Job Room

To receive live location updates for an active job, join its room:

```dart theme={null}
// Join
socket.emit('job:join', {'jobId': 'uuid-of-the-job'});

// Leave when done
socket.emit('job:leave', {'jobId': 'uuid-of-the-job'});
```

## Events Reference

### Client → Server (Emit)

#### `location:update`

**Who:** Mechanic only (when `en_route`, `arrived`, or `in_progress`)

Send GPS position updates. Emit every \~5 seconds while actively on a job.

```dart theme={null}
socket.emit('location:update', {
  'lat': 12.9716,
  'lng': 77.5946,
  'heading': 180.5,   // degrees, 0-360
  'speed': 25.3,      // km/h
});
```

<Note>
  Rate limit: **60 updates/minute**. Exceeding this silently drops updates. The server debounces database writes to every 30 seconds but updates Redis/Valkey immediately for real-time delivery.
</Note>

***

#### `job:join`

**Who:** Customer or assigned mechanic

Join a job's tracking room to receive `location:updated` and `job:status` events for that job.

```dart theme={null}
socket.emit('job:join', {'jobId': 'a1b2c3d4-...'});
```

Rate limit: **10/minute**

***

#### `job:leave`

**Who:** Customer or assigned mechanic

Leave a job's tracking room. Do this when navigating away from the tracking screen.

```dart theme={null}
socket.emit('job:leave', {'jobId': 'a1b2c3d4-...'});
```

Rate limit: **10/minute**

***

### Server → Client (Listen)

#### `job:offer`

**Room:** `mechanic:{mechanicProfileId}`
**When:** A new job is dispatched to nearby mechanics.

```dart theme={null}
socket.on('job:offer', (data) {
  // data = {
  //   "jobId": "uuid",
  //   "category": "Flat Tire",
  //   "pickupAddress": "MG Road, Bangalore",
  //   "basePriceDisplay": "₹350",
  //   "distanceKm": 2.4
  // }
  showJobOfferDialog(data);
});
```

<Note>
  The mechanic has a limited window to accept (30–45s depending on dispatch tier). After timeout, the offer moves to the next group of mechanics.
</Note>

***

#### `job:taken`

**Room:** `mechanic:{mechanicProfileId}`
**When:** A job the mechanic was offered has been accepted by another mechanic.

```dart theme={null}
socket.on('job:taken', (data) {
  // data = { "jobId": "uuid" }
  dismissJobOfferDialog(data['jobId']);
});
```

***

#### `job:status`

**Room:** `job:{jobId}` or `user:{userId}`
**When:** Any job status transition occurs.

```dart theme={null}
socket.on('job:status', (data) {
  // data = {
  //   "jobId": "uuid",
  //   "status": "en_route",
  //   "mechanic": { "id": "uuid", "name": "Ravi", "phone": "+91..." },  // on accepted
  //   "finalPrice": 45000  // in paisa, on completed
  // }
  updateJobStatus(data);
});
```

***

#### `job:line-item-added`

**Room:** `job:{jobId}`
**When:** Mechanic adds a part or labor charge during the job.

```dart theme={null}
socket.on('job:line-item-added', (data) {
  // data = {
  //   "jobId": "uuid",
  //   "lineItem": {
  //     "id": "uuid",
  //     "description": "Spark plug replacement",
  //     "quantity": 2,
  //     "unit_price": 15000,  // paisa
  //     "type": "part"
  //   }
  // }
  addLineItemToUI(data['lineItem']);
});
```

***

#### `location:updated`

**Room:** `job:{jobId}`
**When:** Mechanic's position is updated (every \~5s during active job).

```dart theme={null}
socket.on('location:updated', (data) {
  // data = {
  //   "lat": 12.9716,
  //   "lng": 77.5946,
  //   "heading": 180.5,
  //   "speed": 25.3,
  //   "etaSeconds": 420
  // }
  updateMapMarker(data['lat'], data['lng'], data['heading']);
  updateEtaDisplay(data['etaSeconds']);
});
```

<Note>
  ETA is calculated as `distance / 30 km/h` (assumed urban India speed) and cached with a 30s TTL. Use this for the "X min away" display.
</Note>

***

#### `payment:failed`

**Room:** `user:{userId}`
**When:** A Razorpay payment attempt fails.

```dart theme={null}
socket.on('payment:failed', (data) {
  // data = {
  //   "jobId": "uuid",
  //   "razorpayOrderId": "order_xyz"
  // }
  showPaymentFailedDialog(data);
});
```

***

#### Call signaling events

Call signaling uses 4 additional socket events: `call:accepted`, `call:declined`, `call:missed`, and `call:cancelled` (FCM only). See the [Voice Calls](/voice-calls) guide for the full call flow, FCM payloads, and nonce exchange.

***

## Complete Flutter Integration Example

```dart theme={null}
class SocketService {
  late io.Socket socket;

  void connect(String accessToken) {
    socket = io.io(
      'https://mechzie-api-production.run.app',
      io.OptionBuilder()
        .setTransports(['websocket'])
        .disableAutoConnect()
        .setAuth({'token': accessToken})
        .build(),
    );

    socket.onConnect((_) => _onConnected());
    socket.onDisconnect((_) => _onDisconnected());
    socket.onConnectError((err) => _handleAuthError(err));

    // Listen to all server events
    socket.on('job:offer', _handleJobOffer);
    socket.on('job:taken', _handleJobTaken);
    socket.on('job:status', _handleJobStatus);
    socket.on('job:line-item-added', _handleLineItem);
    socket.on('location:updated', _handleLocationUpdate);
    socket.on('payment:failed', _handlePaymentFailed);

    // Call signaling (see voice-calls guide)
    socket.on('call:accepted', _handleCallAccepted);
    socket.on('call:declined', _handleCallDeclined);
    socket.on('call:missed', _handleCallMissed);

    socket.connect();
  }

  void joinJob(String jobId) => socket.emit('job:join', {'jobId': jobId});
  void leaveJob(String jobId) => socket.emit('job:leave', {'jobId': jobId});

  void sendLocation(double lat, double lng, double heading, double speed) {
    socket.emit('location:update', {
      'lat': lat, 'lng': lng, 'heading': heading, 'speed': speed,
    });
  }

  void reconnectWithNewToken(String newToken) {
    socket.disconnect();
    socket.io.options['auth'] = {'token': newToken};
    socket.connect();
  }

  void disconnect() => socket.disconnect();
}
```

## HTTP Fallback for Tracking

If Socket.io is unavailable, you can poll the mechanic's location via REST:

```bash theme={null}
GET /api/v1/tracking/jobs/{jobId}
Authorization: Bearer ACCESS_TOKEN
```

**Response:**

```json theme={null}
{
  "mechanicLat": 12.9716,
  "mechanicLng": 77.5946,
  "etaSeconds": 420,
  "updatedAt": "2026-05-21T10:30:00.000Z"
}
```

<Warning>
  HTTP tracking is rate-limited to **1 request every 2 seconds**. Prefer Socket.io for real-time updates — it pushes updates every \~5 seconds without polling overhead.
</Warning>
