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:
- 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.
- 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_idis 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.
Related¶
- Customer Tunnels — different topic, but the WireGuard tunnel is what keeps
failover_phone_numberrarely 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 insidegenerateUserExtension.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.