
In an illustrative scenario, an operations colleague asks: “We sent 10,000 campaign notifications yesterday. Why did only 200 people open them?” The backend developer checks the logs: “Every API call returned success.”
They mean different things by success. The backend means the vendor accepted the request. Operations means a notification appeared on the user's phone. Offline devices, revoked notification permission, vendor rate limits, battery-saving behavior and expired tokens can intervene.
Based on AppKernia's push design in ADR-0021, this article covers three questions: how to separate push states, how to classify failures, and when to request notification permission.
1. Why connect directly to nine channels?
AppKernia targets the App Store, Google Play, domestic Android stores and HarmonyOS NEXT. The server integrates these channels directly:
- APNs over HTTP/2 and FCM over HTTP v1;
- Huawei on Android, Honor, Xiaomi, OPPO, vivo and Meizu;
- HarmonyOS Push Kit.
There is no third-party push aggregator. Each vendor needs its own adapter, but credentials, limits and error codes remain under the project's control, with fewer layers to investigate when something fails.
Android has two mutually exclusive build variants: android_google and android_china. Bundling every vendor SDK into one package makes size and privacy compliance harder. The overseas build includes FCM; the China build includes domestic vendor channels. Unsupported devices retain in-app messages instead of attempting unsupported push delivery.
2. “Push success” comprises four stages

A state diagram, not a measurement of time spent in each stage.
| State | Meaning | Who can verify it? |
|---|---|---|
| Business event created | A work order was assigned to an engineer, for example | Your server |
| Vendor accepted | The vendor received the send request | The vendor API response |
| Device displayed | The notification appeared on the phone | Most channels cannot reliably report this |
| User opened | The user tapped the notification and opened the app | An app report |
In AppKernia, sent means only vendor acceptance. The initial metrics cover acceptance, failure, invalid tokens and app opens. They do not claim device display.
“10,000 accepted, 200 opened” is useful information. Calling those 10,000 messages delivered can send an investigation in the wrong direction.
3. Seven send outcomes determine retry behavior

Diagram of AppKernia's send-result classifications.
Vendor error codes differ. AppKernia maps them to seven outcomes with explicit handling:
| Outcome | Meaning | Handling |
|---|---|---|
accepted | The vendor accepted the request | Record acceptance |
invalid_token | The device token is invalid | Invalidate the binding immediately and stop sending |
throttled | The vendor applied a rate limit | Back off according to Retry-After |
transient | A definite temporary failure, such as a 5xx response | Retry with exponential backoff and jitter |
permanent | A permanent failure | Record the reason without retrying |
auth_config_error | Authentication or configuration is wrong | Mark the channel faulty and wait for operator action |
unknown_after_write | The request was written, but its result is unknown | Do not replay automatically |
The last outcome deserves special attention. A connection can fail after the request has left the client but before its response arrives. The vendor may already have accepted and sent the message, or may not have received it. Retrying can produce duplicate notifications, which are especially confusing for verification codes and order updates.
The rule is: connection failures before writing may be retried; unknown results after writing are not replayed. This favors avoiding duplicates. In-app messages do not depend on successful push delivery, so users can still see them when they open the app.
4. Ask for permission after consent
An app that requests notification permission immediately gives users little reason to agree. Initializing a push SDK and uploading a device token before privacy consent also violates the project's consent boundary.
The ak-push module is the only push entry point available to mobile business code:
Until both required legal consent and the user's master push switch are enabled, do not request notification permission, initialize SDKs, obtain tokens or upload them.
Four gates determine whether a notification can be sent:

All four conditions must hold.
- Global kill switch: stop sends centrally when needed.
- App and environment configuration: isolate vendor credentials by tenant, app, environment and vendor; encrypt them and support version rotation.
- User subscription: do not send when the user has disabled push.
- System permission: deactivate the device binding when notification permission is revoked in system settings.
Logout and account changes also deactivate bindings. Otherwise, user B could receive user A's work-order notifications after signing into the same phone.
Campaign push (news_operations) and service or security push (service_security) are separate protocol categories. Campaign push is off by default and requires notify.operations.publish. HarmonyOS and vivo vendor scenario values are configured separately for these categories.
5. Notification destinations need an allowlist
Tap payloads may contain only a schema version, delivery or message identifier, controlled route_key and opaque resource identifier. They may not contain arbitrary URLs, component names, scripts or instructions for silent background execution.
A payload passing through a vendor server does not make it trustworthy. After a tap, the app uses the resource identifier to fetch the resource with fresh server authorization. If a work order has been reassigned, the user should see an access-denied state rather than the content from the notification.
6. Release acceptance matrix
Compilation, mocks and valid contracts do not replace vendor acceptance. Before enabling a channel, verify:
- Vendor account entitlements—for example, HarmonyOS Push Kit activation followed by renewed debug and release profiles;
- Matching credentials, package or Bundle ID and signing fingerprints;
- Privacy compliance and network review;
- Real devices in foreground, background, killed-process and offline-recovery states;
- Permission denial, invalid tokens, taps while signed out and resources the user can no longer access.
Channels that have not passed remain disabled. Tokens are encrypted on the server and deduplicated with an HMAC hash. Logs, audits, metrics and management APIs do not expose raw tokens or credentials.
Closing thoughts
The difficult part of push is explaining each failure clearly. Separate states, classify outcomes and fix retry rules so operations can trust the metrics and users avoid duplicate messages.
ak-push and its ak-permissions dependency are available as free source packages in AppKernia's plugin directory. The full server implementation is in the MIT-licensed repository:
- Source: github.com/Payhon/AppKernia
- Documentation: docs.appkernia.com, including push-channel configuration
The source does not include your vendor accounts or signing materials. Each channel still needs acceptance in your project with your own credentials.