Skip to content

RFC: iOS Critical Alerts for OneUptime Mobile Push Notifications #2529

Description

@HanCheo

relate #2511

Summary

Enable iOS Critical Alerts for incident-triggered push notifications sent to on-call responders when the incident severity is explicitly marked as Critical Alert eligible by the project.

This requires:

  1. Apple entitlement approval for com.apple.developer.usernotifications.critical-alerts.
  2. iOS app entitlement/config updates in MobileApp.
  3. Server-side Expo push payload changes to send interruptionLevel: "critical" only when the incident severity is configured by the project as Critical Alert eligible.
  4. Narrow scoping so only severity-approved incident-driven on-call notifications use Critical Alerts.

Problem

Today the iOS app requests notification permissions and already asks for allowCriticalAlerts, but the app and push pipeline do not complete the full critical-alert path.

Observed current state:

  • MobileApp/src/notifications/setup.ts requests iOS permission with allowCriticalAlerts: true.
  • MobileApp/app.json configures expo-notifications and UIBackgroundModes, but does not declare the iOS critical alerts entitlement.
  • Common/Server/Services/PushNotificationService.ts sends Expo messages with:
    • sound: "default"
    • priority: "high"
    • Android channelId
    • no interruptionLevel
  • OneUptime mobile push is routed through Expo Push (MobileApp/README.md, App/FeatureSet/Docs/Content/en/self-hosted/push-notifications.md).

Result: on-call notifications cannot reliably break through iOS silent / DND / Focus modes the way dedicated incident-response apps do.

Goals

  • Deliver incident-created notifications to on-call responders as elevated notifications when the incident severity is marked Critical Alert eligible by the project:
    • iOS uses Critical Alerts
    • Android uses the oncall_critical notification channel
  • Keep non-critical-severity mobile pushes on the existing high-priority path.
  • Preserve Expo Push as the transport.

Non-Goals

  • Replacing Expo Push with direct APNs.
  • Making every mobile notification critical.
  • Reworking notification routing, persistence, or token registration.
  • Solving app-store / Apple review process in code alone.

Constraints

Apple entitlement requirement

Apple requires the com.apple.developer.usernotifications.critical-alerts entitlement. Without it, the app may request critical permissions but cannot legally/functionally deliver true Critical Alerts.

Reference: Apple docs say the entitlement permits an app to receive critical alert notifications and must be requested from Apple.

Expo transport requirement

Expo Push supports iOS interruptionLevel, including critical, in the push payload format. Expo is therefore not the blocker by itself; the missing piece is entitlement + payload selection + deployment.

Current Architecture

Mobile app

Relevant files:

  • MobileApp/app.json
  • MobileApp/src/notifications/setup.ts
  • MobileApp/src/hooks/usePushNotifications.ts

Current behavior:

  • Registers Expo push token on login.
  • Requests notification permissions.
  • Creates Android notification channels only.
  • Does not define iOS entitlements for critical alerts in app config.

Push send path

Relevant files:

  • Common/Server/Services/PushNotificationService.ts
  • App/FeatureSet/Notification/API/PushRelay.ts
  • Common/Server/Services/UserNotificationRuleService.ts

Current behavior:

  • On-call alerts/incidents/pages are sent through PushNotificationService.sendPushNotification(...).
  • Expo payloads are assembled centrally in PushNotificationService.ts.
  • Relay path mirrors the same payload shape.
  • There is currently no way to mark a push as iOS-critical.

Proposal

1. Add iOS critical alerts entitlement to the app config

Update MobileApp/app.json to declare the entitlement for iOS builds.

Proposed config shape:

  • expo.ios.entitlements["com.apple.developer.usernotifications.critical-alerts"] = true

This must ship only after Apple approves the entitlement for the production app identifier.

2. Keep permission request logic, but verify/observe it

MobileApp/src/notifications/setup.ts already requests:

  • allowAlert: true
  • allowBadge: true
  • allowSound: true
  • allowCriticalAlerts: true

No major structural change is required here. Optional improvement:

  • add explicit logging / telemetry for permission result so support can distinguish:
    • standard notification approval
    • critical-alert approval denied

3. Extend server push payloads with platform-specific critical elevation

Add a first-class way for the server to express that a push should be elevated for critical incident delivery.

Proposed approach:

  • extend PushNotificationOptions in Common/Server/Services/PushNotificationService.ts with something like:
    • isCriticalAlert?: boolean

Then in payload assembly (sendExpoPushNotification, sendViaRelay, sendRelayPushNotification):

  • when isCriticalAlert === true:
    • iOS includes interruptionLevel: "critical"
    • Android uses channelId: "oncall_critical"
  • when not critical:
    • preserve the existing high-priority path
    • keep current sound handling

4. Propagate the flag through relay API

Update App/FeatureSet/Notification/API/PushRelay.ts to accept and forward the new field to PushNotificationService.sendRelayPushNotification(...).

This keeps both direct Expo and relay-driven paths consistent.

5. Mark only severity-approved incident-driven on-call notifications as critical

Do not mark all pushes as critical.

Initial scope should be limited to the push calls made from on-call escalation flows in:

  • Common/Server/Services/UserNotificationRuleService.ts

The project must explicitly decide which incident severities are Critical Alert eligible. Proposed implementation:

  • add a boolean field to IncidentSeverity, for example isCriticalSeverity
  • set isCriticalAlert: true only when:
    • the notification is incident-driven, and
    • incident.incidentSeverity?.isCriticalSeverity === true

This avoids guessing from names like P1 or Critical, and avoids inferring from order.

For the first release, boring/default behavior is:

  • keep the existing high-priority path as-is for all current mobile notifications
  • elevate only incident-driven notifications sent to on-call users with Critical Alert eligible severities:
    • iOS => interruptionLevel: "critical"
    • Android => channelId: "oncall_critical"
  • do not introduce a broader normal/high/critical redesign in this change

Implementation Plan

Mobile app changes

  1. Update MobileApp/app.json with iOS entitlement.
  2. Rebuild iOS app with approved Apple capability.
  3. Verify the generated iOS entitlements in prebuild/native output.

Web UI changes

  1. Extend IncidentSeverity settings UI so project admins can mark a severity as Critical Alert eligible.
  2. Update the Incident Severity form/table in App/FeatureSet/Dashboard/src/Pages/Incidents/Settings/IncidentSeverity.tsx.
  3. Surface the new boolean field from Common/Models/DatabaseModels/IncidentSeverity.ts.

Server changes

  1. Extend IncidentSeverity with a Critical Alert eligibility flag.
  2. Add critical-alert option to push send API.
  3. Add interruptionLevel: "critical" support for iOS payloads.
  4. Add channelId: "oncall_critical" support for Android payloads.
  5. Add relay request/response support.
  6. Set the elevated path only when the incident severity is flagged as Critical Alert eligible.
  7. Preserve the existing high-priority path for all non-critical notifications.

Upstream Release Dependencies

The following items are outside the implementation scope of this RFC and must be handled by the OneUptime mobile app release owner:

  1. Request and obtain Apple approval for the com.apple.developer.usernotifications.critical-alerts entitlement.
  2. Build a new iOS app binary with the approved entitlement.
  3. Release the updated mobile app through the normal OneUptime distribution path.

Testing Strategy

Code-level

  • Unit tests for payload construction in PushNotificationService.ts:
    • iOS critical => interruptionLevel: "critical"
    • iOS non-critical => no critical interruption level
    • Android critical => channelId: "oncall_critical"
    • Android non-critical => existing high-priority channelId behavior preserved
  • Relay tests for forwarding the new field in PushRelay.ts.

Release validation

Test on physical devices using the released entitlement-enabled build:

  1. Install the updated mobile build.
  2. Grant notification + critical alert permissions on iPhone.
  3. Trigger an incident-created notification to an on-call responder whose incident severity is marked Critical Alert eligible.
  4. Verify iOS audible delivery during:
    • silent switch enabled
    • Do Not Disturb / Focus enabled
  5. Verify Android delivery uses the oncall_critical channel for the same incident severity.
  6. Verify incidents with non-eligible severities remain on the existing high-priority path.

Docs to Update After Shipping

  • MobileApp/README.md
  • App/FeatureSet/Docs/Content/en/self-hosted/push-notifications.md
  • App/FeatureSet/Docs/Content/en/mobile-desktop-apps/ios-installation.md

Add guidance for:

  • entitlement dependency
  • critical alert permission prompt
  • expected behavior for incident-created on-call notifications only when the incident severity is marked Critical Alert eligible

Minimal File List

Primary implementation files:

  • MobileApp/app.json
  • MobileApp/src/notifications/setup.ts (likely small/no-op code change; verify logging only)
  • Common/Models/DatabaseModels/IncidentSeverity.ts
  • App/FeatureSet/Dashboard/src/Pages/Incidents/Settings/IncidentSeverity.tsx
  • Common/Server/Services/PushNotificationService.ts
  • App/FeatureSet/Notification/API/PushRelay.ts
  • Common/Server/Services/UserNotificationRuleService.ts

Optional/supporting files:

  • tests near PushNotificationService.ts
  • docs listed above

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions