Skip to content

Mobile App Access (WSS softphone line)

Status: ACTIVE — platform side on staging (PR #515 + #516 merged; #516 decouples Mobile App Access from Call Routing). Staging E2E verified 2026-07-10: toggling sip_port via PUT /users/:id flips the endpoint transport-udp:5080 ⇄ transport-wss:8089 (+alaw+DTLS) after the ~20s single-org auto-reload; desk lines untouched. Android login gate DONE (commit 6bc29d83 — picker/manual admit only wss lines). iOS login gate DONE too (astracall, 2026-07-12): quick-connect gates on sip_port='wss' + non-empty asterisk_endpoint only — never on routing_type/ring_target. Fixed a bug where the old ring_target != 'phone' clause hid wss lines whose inbound was routed to a SIM/phone. Pending: prod promotion. The "when a line receives inbound calls on the app" decision (call routing) is still deferred — separate from the login gate.

What it is

A per-user "Mobile App Access" control: whether an org user's SIP line can register from the AstraCall mobile app (iOS + Android) over WebRTC/WSS. It replaces the old manual, unmanaged workflow (UPDATE users SET sip_port='wss' + regen by hand) with a managed, admin-controlled toggle surfaced in the editor and reflected in the app.

The model: two ORTHOGONAL axes

Mobile App Access and Call Routing are independent — the first cut wrongly coupled them (the Mobile toggle was hidden for SIM/AI-routed lines, and "Mobile App" was jammed into the routing dropdown). Corrected 2026-07-09 (revised again):

  1. Mobile App Access = sip_port='wss' (a WebRTC endpoint). Controls whether the line can log in from the AstraCall app. On/off, shown for every line regardless of routing.
  2. Call Routing (routing_type / ring_target) = where inbound calls ring. Unchanged 3 options: SIP / IP Phone (sip/ext), AI Bot (ai_agent), Phone Call = SIM (sip/phone, rings cellular over PSTN). No "Mobile App" routing option — that conflated login with routing.

Any combination is valid — e.g. Mobile App Access ON + routing = Phone (SIM): the user logs into the app (outbound click-to-call / tickets / presence) while inbound rings their SIM.

sip_port Endpoint flavour App can log in?
5080 (default) / 5060 (legacy) UDP desk/ATA no
wss transport-wss + webrtc yes

Deferred: whether a line should actually receive inbound calls on the app (vs SIM/desk) is a separate call-routing decision, to be designed later (on mobile or in the editor). Mobile App Access is only about login — never a receive-call gate.

Data model — no new column

users.sip_port='wss' is the single source of truth, already validated end-to-end (api/src/services/asterisk/sipEndpointOptions.js, SIP_PORTS=['5080','5060','wss']) and already wired to config regen. Everywhere else it's exposed as a derived boolean mobile_enabled = (sip_port === 'wss'). No migration: lines flipped manually before this (e.g. Om Chamber ext 1001) already read wss and light up automatically.

  • The generator (userProvisioningService.js) emits transport=transport-wss, webrtc=yes, allow=alaw for wss lines; UDP desk/ATA endpoints stay byte-identical. See concurrent-call-cap.md — each wss line is a transcode/CPU cost, which is why access is admin-granted.
  • Toggling off sets sip_port='5080' (a former 5060 legacy line reverts to 5080 — accepted; 5060 is a desk-hardware SIP-ALG workaround that wouldn't be a mobile line).

API — reuse PUT /api/v1/users/:id

  • Enable: PUT /api/v1/users/:id {"sip_port":"wss"} → 200 (updated user).
  • Disable: {"sip_port":"5080"}.
  • Regen: already automatic — a sip_port change fires a single-org, background Asterisk reload (not restart; live calls safe) via the existing mutex'd deploy path. No new route, no check:routes update.
  • RBAC (admin-only): the route is member-accessible for self-service fields, so the sip_port guard is field-level — enforced only when sip_port actually changes. A non-admin JWT gets 403; master/internal keys bypass (same as requireRole). Pinned by api/tests/mobile-access-admin-only.test.js. Rationale: each wss line is a capacity cost, so it's granted, not self-claimed.

Editor UI (editor/app/dashboard/[orgId]/users/page.tsx)

  • Edit dialog → "Mobile App Access" switch (next to Health Check) — independent of Call Routing. On → sip_port='wss'; off → 5080. The SIP Port select (5080/5060) shows only while Mobile App Access is off (WSS has no port choice) — this also fixes the blank-select bug on already-wss lines.
  • Edit dialog → Call Routing is the original 3 options (SIP / AI Bot / Phone-SIM), untouched — it no longer changes sip_port.
  • Users list → "Mobile" column next to "Mobile Push": an inline on/off Switch on every row (including SIM/Phone- and AI-routed lines) for at-a-glance "who can log in on the app" + one-click grant.
  • Type widened: sip_port?: "5080" | "5060" | "wss" (editor/lib/pbx/client.ts).
  • Add-user form intentionally unchanged — create a user, then flip Mobile on.

Mobile apps — gate login on the Mobile App Access toggle

Decision (2026-07-10, final): the mobile app logs in only lines whose "Mobile App Access" toggle is ON (sip_port='wss'). Everything else is not provisioned for the app.

Reconcile the two rules that sound opposite but aren't:

  • Do NOT gate on call-routing type. A wss line whose inbound rings the SIM (ring_target='phone', e.g. staging ext 1003) still logs in. Routing ≠ access.
  • DO gate on the toggle (sip_port='wss'). Non-wss SIP lines are excluded from the picker and rejected on manual entry.

Behaviour (both platforms): - Line picker: shows only sip_port='wss' lines. Non-mobile lines are not listed. - Manual entry / QR: a typed/scanned user that isn't a wss line is rejected with "This SIP user is not provisioned" — checked against the org's mobile-enabled set before registering. (The 20s register-timeout remains as a backstop.) - Both read sip_port from GET /api/v1/users (already returned).

Android (Mobile App/astradial-android, branch private-main) — DONE (commit 6bc29d83)

  • AstradialApi.getOrgSipUsers() gates on sip_port=='wss' (was routing_type=sip && ring_target!=phone); exposes endpoint (PJSIP username) on OrgSipUser. Picker, in-app "change line", and cold-start re-provision all consume it → gated for free.
  • OneUiSettingsFragment.showManualDialog rejects a user not in the mobile-enabled set with astradial_sip_not_provisioned before provisioning.
  • No QR-scan provisioning exists in the Astradial overlay — nothing to gate there.

iOS — DONE (2026-07-12, the App Store app astradial-astracall)

The store app is Mobile App/astradial-astracall (com.astradial.astracall), NOT the old Linphone fork astradial-ios. Implemented there:

  1. OrgUser struct already carries sipPort (sip_port) + asteriskEndpointAstraCall/Screens/NativePhoneUI.swift (computed mobileEnabled = sipPort == "wss").

  2. Quick-connect filterAstraCall/Screens/OnboardingUI.swift, OnboardingSIPQuickView.sipUsers:

    users.filter { $0.mobileEnabled && !($0.asteriskEndpoint ?? "").isEmpty }
    
    Gates ONLY on the Mobile App Access toggle (sip_port='wss') + a real endpoint — never on routing_type/ring_target. This fixed a bug where the old ring_target != 'phone' clause hid wss lines whose inbound was routed to a SIM/phone (they must still register). AI agents (sip_port='5080') stay hidden unless an admin grants Mobile access.

  3. Manual entry / QR — Manual and Scan QR are separate Connect-Type tabs on the "Connect Your Line" screen; provisioning re-guards mobileEnabled before registering.

Interactions

  • SIM Calls Only (ring_target=phone) controls where inbound calls ring; it is orthogonal to Mobile App Access. A wss line whose inbound rings the SIM can STILL register from the app (it just receives calls on the SIM). The pickers gate on sip_port='wss' only, never on ring_target — do not re-add a routing exclusion.
  • Codec: wss lines are allow=alaw (already settled — no FEC, so cellular is choppy; the SIM path is the cellular answer). See [AstraCall codec constraints].
  • Capacity: cost is per concurrent webrtc call (transcode), not per registered line; no hard cap in v1 — the editor "Mobile" column/count is the operator's self-police signal.
  • VoIP push tokens: a token is registered only after a successful WSS registration, so a non-wss line naturally never registers a token (it can't register at all) — no login gate and no server-side push-token change needed.

VoIP push registration on app open (2026-07-11)

The app ensures its device push token is registered on every app open (not just when the token first arrives), so a mobile line reliably wakes for inbound calls.

Flow (strict, permission-gated): on app foreground (iOS scenePhase == .active, and when the SIP line reaches .registered; Android onResume): 1. SIP line registered + endpoint known? If no → no-op. 2. Push token delivered yet? If no → skip (the token-arrival callback re-enters later). 3. GET /api/v1/users/voip-push-status?asterisk_endpoint=…&token=… → if registered && token_matchesdone (no redundant POST). 4. Otherwise, notification-permission gate: authorized → register (POST /users/voip-push-token); not-yet-asked (on an app-open) → prompt, register on grant; denied → do NOT register. Registration is intentionally gated on notification permission even though VoIP push technically doesn't need it.

Endpoints (org-scoped via authenticateOrg, keyed by asterisk_endpoint to match the POST): - GET /api/v1/users/voip-push-status{registered, token_matches, environment, platform, updated_at}. Never returns the raw token. token_matches:false on a rotated token (reinstall/new device) → app re-registers. 404 if no user for that endpoint in the org. App treats non-200/network error as "not registered" and falls back to registering (graceful — an un-deployed server never breaks the app). - DELETE /api/v1/users/voip-push-token?asterisk_endpoint=…self-deregister for the app's Settings → Revoke Notification Access (clears the token; the wake CURL drops from the dialplan on the next regen). Distinct from the editor's admin DELETE /users/:id/voip-push-token. iOS can't revoke its own OS notification permission, so Revoke = deregister server-side and deep-link to iOS Settings.

Bundle id is fixed per platform (not free-text): iOS com.astradial.astracall, Android com.astradial.phone. The editor's Mobile Push dialog derives it from the platform (field disabled).

Implementations: iOS PushKitManager.ensureRegisteredOnOpen() (Mobile App/astradial-astracall). Android parity is speced in VOIP-PUSH-ON-OPEN-HANDOFF.md in the android repo (FCM + POST_NOTIFICATIONS; Pulse analytics is a no-op stub there).

Rollout

  1. API + Editor (this branch) → PR to staging → verify on stageeditor: toggle a staging user, confirm pjsip show endpoint shows transport-wss / webrtc: true after the auto reload, toggle off, confirm revert; confirm zero diff on untouched desk lines.
  2. Hari promotes staging → main (prod). Existing manually-flipped prod lines keep working. (PR #515 merged to staging 2026-07-09.)
  3. Mobile apps: no login/picker change — login stays unrestricted by routing type (see above). The "when does a line receive calls on the app" decision is deferred; revisit here when designed.
  4. Docs finalised here once staging E2E confirms.