Skip to main content

Overview

MechZie audio calls use 100ms for the actual audio room and a custom FCM + Redis signaling layer for ringing. 100ms has no concept of “ringing” — it only knows join/leave room. The signaling layer bridges that gap.
Who can call whom? Either party (customer or mechanic) can initiate a call on a job once it is in accepted, en_route, arrived, in_progress, completed, or disputed status.

Architecture

FCM payloads are data-only (no notification key). This ensures Flutter’s background message handler fires even when the app is killed. Adding a notification key will break call wake-up on Android when the app is backgrounded.

Call Flow — Step by Step

1. Caller initiates

Response 201:
The caller immediately uses hms_room_id + auth_token to join the 100ms room and wait. The other party’s phone rings via FCM.

2. Receiver’s FCM payload

The receiver’s Flutter background handler receives:
Store the nonce in memory only — never log or persist it. It is single-use and expires in 5 minutes.

3. Receiver accepts

Response 200:
The nonce is consumed (single-use). A fresh HMS token is generated for the receiver. The caller receives a call:accepted Socket.io event. Both parties are now in the 100ms room.

4. Receiver declines

Response 200:
Caller receives call:declined on user:{callerId} Socket.io room.

5. Caller cancels (before answer)

Response 200:
A cancellation FCM is sent to dismiss the receiver’s native call UI. Caller receives no socket event (they initiated the cancel).

6. Either party ends the call (hangup)

Call this from leaveCall() in both Flutter apps whenever a party hangs up:
Response 200:
The active call state is cleared from Redis immediately. Both parties receive a call:ended Socket.io event so each app can return to its idle UI. Idempotent — safe to call multiple times (no-ops if call already ended).
Always call /call/end from your leaveCall() handler regardless of who initiates the hangup. If both parties call it simultaneously, only the first clears Redis; the second is a silent no-op.

7. Missed call (timeout)

If the receiver doesn’t answer within 30 seconds, a BullMQ worker fires:
  • Deletes the ringing state from Redis
  • Emits call:missed to the caller on user:{callerId}
  • Writes a call_missed audit entry to job_messages

Socket.io Events (Call Signaling)

Listen on your connected Socket.io instance. All events are delivered to the user:{userId} room (auto-joined on connect).

call:accepted


call:declined


call:missed


call:ended

Fired to both parties when either one calls /call/end. Use this to return your UI to idle when the remote party hangs up first.

call:cancelled (FCM only)

Delivered as a data-only FCM message (not a Socket.io event) so it wakes the app even when killed:

Flutter Integration Example


Error Codes


Redis State Machine

The 35 s TTL on call:ringing is a safety net — the BullMQ timeout fires at 30 s and cleans up Redis itself. The extra 5 s prevents a race where Redis expiry fires before the worker. The 4 h TTL on call:active is a safety net only/call/end clears it immediately on hangup. The TTL prevents the key from persisting indefinitely if both clients crash without calling the endpoint.