Skip to content

User Failover — Active vs Inactive Routing

Each SIP user has TWO independent failover fields with different semantics. The distinction is non-obvious and has caught engineers reviewing the dialplan generator. This doc is the canonical reference.

The two fields

Column on users Meaning Fires when
failover_phone_number "If MY softphone has network trouble, also try MY mobile" — resilience for the same person User is active AND primary endpoint returns NOANSWER / BUSY / CHANUNAVAIL / CONGESTION
failover_destination_user_id "Cover for me when I'm off-shift" — a different staff member takes the call User is inactive (status flipped off), OR active primary fails on an active user

They are mutually exclusive at the API layer (you set one or the other, never both).

Why two fields, not one

The two cover different real-world cases:

  1. Resilience: Ramesh is on shift. His desk softphone's WAN flaps. The caller should still reach Ramesh on his mobile. The mobile is Ramesh's number — the person dialed is still Ramesh.
  2. Shift coverage: Ramesh's shift ended at 6 PM; Priya is on the night shift. Calls to extension 1006 should ring Priya's extension (1007). The caller dialed Ramesh, but a different person answers. This is editorial routing, not network resilience.

If we collapsed both into one "failover" field, a Saturday-evening caller dialing Ramesh's extension would either: - (a) Ring Ramesh's personal mobile when he's off-shift (privacy + on-call violation), or - (b) Skip the colleague-coverage step entirely if Ramesh's softphone is up but he's not there.

Two fields, two semantics — neither outcome is possible.

What happens when you toggle a user inactive

Toggling the Active switch off in the editor → users.status = 'inactive' → debounced 750 ms → AMI reload.

PJSIP layer

The user is excluded from generated pjsip.conf (userProvisioningService.js:16). After the reload:

  • The user's softphone can no longer REGISTER (401 on next attempt; existing session drops).
  • Inbound calls to that endpoint URI fail at the SIP layer.

Dialplan layer (dialplanGenerator.js:139-153)

Two branches:

Inactive user has… Dialplan emitted? Behaviour for callers
failover_destination_user_id → an active colleague YES — redirect-only entry that Dials the colleague directly Caller rings the colleague. If colleague doesn't answer either, caller hears "the person at extension N is not available".
failover_destination_user_id is NULL or points to an inactive colleague NO — extension omitted entirely Caller hits Asterisk's unknown-extension fallback: "the number you have dialed is not in service".
Only failover_phone_number set (no colleague-failover) NO — extension omitted (intentionally) Same "not in service" — because the user is off-shift, their own mobile shouldn't ring.

The third row is the non-obvious one. The dispatcher at dialplanGenerator.js:144 deliberately checks only failover_destination_user_id — not failover_phone_number — when deciding whether to emit a dialplan entry for an inactive user. This is correct: the per-person mobile failover is a network-resilience feature, not a shift-coverage feature.

Existing in-progress calls

Unaffected. Asterisk only reads dialplan when routing a new channel, so a call already connected to the now-inactive user stays up until natural hangup.

Decision matrix

User status Primary softphone reachable failover_phone_number failover_destination_user_id Caller experience
active yes (any) (any) Softphone rings.
active no (NOANSWER/BUSY/etc) set Falls over to user's own mobile.
active no set (→active user) Falls over to colleague's extension.
active no "person at extension N is not available".
inactive (any) set (→active user) Calls rerouted to colleague — primary user's mobile is NOT rung.
inactive set — / unset "number not in service" — own mobile is NOT rung.
inactive "number not in service".

Configuration (editor UI)

Per user, on the Users page:

  • Failover SIP user dropdown → sets failover_destination_user_id. Filtered to same-org active users only. Cross-org refs forbidden server-side.
  • Failover phone number input → sets failover_phone_number. E.164-ish (+91 + 10 digits). Server returns 400 if both fields are set in the same PATCH.
  • Failover timeout (seconds) input → sets failover_timeout_seconds (default 20, bounded 5-120). How long the primary rings before falling over.

Edge cases worth knowing

  • Cascading failover is single-hop. If A → B → C and B is also inactive, the call does NOT chain to C. Dispatcher requires the immediate failover target to be active. Multi-hop chaining is a known v1 limitation.
  • Inactive ring_target='phone' user (no SIP endpoint, just a mobile-routed user) → toggling inactive still removes the extension from dialplan unless a failover_destination_user_id is set. Same semantics: their own mobile shouldn't ring when off-duty.
  • Toggling inactive does not delete the user. Their CDR history, recordings, queue memberships, and password all remain. Toggle back to active → next deploy re-emits the PJSIP endpoint and dialplan entry, and the softphone can register again.
  • Customer Tunnels — different topic, but the WireGuard tunnel is what keeps failover_phone_number rarely needed in the first place (network resilience at the tunnel layer, not the endpoint).
  • Code:
    • api/src/services/asterisk/dialplanGenerator.js — the inactive-user dispatcher (generateOrgDialplan) and the failover branch inside generateUserExtension.
    • api/src/services/asterisk/userProvisioningService.js — PJSIP config generation (active-only filter).
    • api/src/models/User.js — column definitions and validators.
    • editor/app/dashboard/[orgId]/users/page.tsx — Active toggle and failover form fields.