Repository navigation
feat(backend): APNs push notifications for iOS devices - #482
Merged
Merged
Maple Review Bot / Maple / review
succeeded
Nov 14, 2026 in 3m 47s
Confidence 10/10 · No issues found
🟢 Confidence 10/10 · safe to merge. Every source hunk read; ownership handover, outbox retry and count clamping are pinned by tests, with only a real APNs call unverified.
no findings · tests covered · risk high · 5/5 new units observable
Adds backend APNs push: pushDevice.register/unregister, a push.send outbox effect per device, ES256 provider-token signing, and a D1 push_device_tokens owner index that hands a token to its newest owner. Contained and safe to merge; the McpQueryError issues whose frames land in effects/registry.ts are unrelated to this diff and are not addressed by it.
pushDevice.register/unregisterstore a token in the caller's ownUserObjectschedulePushqueues onepush.sendper device, deferred 60 s while the user is activeApnsClientsigns ES256 provider tokens and classifies each APNs refusalpush_device_tokensin D1 moves a token to its newest owner
Before merge
- Env var · Add
APNS_KEY_ID,APNS_TEAM_IDandAPNS_PRIVATE_KEYto the prd and dev secret stores ·apps/backend/src/services/apns.ts:151 - Migration · Check it is safe on production data and with the code still deployed ·
packages/db/migrations/directory/0004_push_device_tokens/migration.sql
Production impact
Open errors in the changed files (2)
| Issue | Service | Occurrences | File |
|---|---|---|---|
| @maple/mcp/errors/McpQueryError (7 issues) | maple-ai | 33 | apps/backend/src/worker/objects/effects/registry.ts |
| @maple/mcp/errors/McpQueryError (3 issues) | maple-chat | 15 | apps/backend/src/worker/objects/effects/registry.ts |
After this merges, Maple checks whether they stop.
Production traffic of the changed files (last 7 days)
| File | Calls/day | Busiest operations |
|---|---|---|
apps/backend/src/services/apns.ts |
20 | ApnsClient.send 20/day, 0.0% err |
Telemetry this change adds and removes (26)
- ➕ attribute
apns.environment·apps/backend/src/services/apns.ts:207 - ➕ attribute
apns.status·apps/backend/src/services/apns.ts:261 - ➕ attribute
apns.reason·apps/backend/src/services/apns.ts:262 - ➕ span name
OrgEffects.forEachGuest·apps/backend/src/worker/objects/org/message-notifications.ts:81 - ➕ span name
OrgEffects.notifiedChannel·apps/backend/src/worker/objects/org/message-notifications.ts:115 - ➕ span name
OrgEffects.messageDeleted·apps/backend/src/worker/objects/org/message-notifications.ts:163 - ➕ attribute
notification.recipients·apps/backend/src/worker/objects/org/message-notifications.ts:201 - ➕ span name
OrgEffects.notificationsRemoved·apps/backend/src/worker/objects/org/notification-count.ts:23 - ➕ span name
OrgObject.pushContent·apps/backend/src/worker/objects/org/push-content.ts:83 - ➕ span name
UserEffects.deleteMessageNotifications·apps/backend/src/worker/objects/user/message-notifications.ts:72 - ➕ span name
Directory.claimPushToken·apps/backend/src/worker/objects/user/push-ownership.ts:108 - ➕ attribute
push.claim·apps/backend/src/worker/objects/user/push-ownership.ts:119 - ➕ span name
Directory.releasePushToken·apps/backend/src/worker/objects/user/push-ownership.ts:146 - ➕ span name
UserObject.forgetPushDevice·apps/backend/src/worker/objects/user/push-ownership.ts:164 - ➕ attribute
push.forgotten·apps/backend/src/worker/objects/user/push-ownership.ts:173 - ➕ attribute
user.forgetPushDevice·apps/backend/src/worker/objects/user/push-ownership.ts:177 - ➕ span name
UserObject.registerDevice·apps/backend/src/worker/objects/user/push.ts:125 - ➕ span name
UserObject.pushDecision·apps/backend/src/worker/objects/user/push.ts:163 - ➕ attribute
push.decision·apps/backend/src/worker/objects/user/push.ts:185 - ➕ attribute
push.skip_reason·apps/backend/src/worker/objects/user/push.ts:186 - ➕ attribute
push.devices·apps/backend/src/worker/objects/user/push.ts:187 - ➕ attribute
push.presence·apps/backend/src/worker/objects/user/push.ts:188 - ➕ span name
UserObject.enqueuePush·apps/backend/src/worker/objects/user/push.ts:207 - ➕ span name
UserObject.sendPush·apps/backend/src/worker/objects/user/push.ts:218 - ➕ attribute
push.skipped·apps/backend/src/worker/objects/user/push.ts:228 - ➕ attribute
push.outcome·apps/backend/src/worker/objects/user/push.ts:304
What was checked
- Device token stays out of telemetry:
TracerDisabledWhendrops both the client span and header propagation (HttpClient.ts:569), andApnsRequestErrorcarries no cause messageDeletednormalises Connect guest/host channel ids throughnotifiedChannelbefore fanning out, so no guest loopnotificationsRemovedis max(0, count−removed) on all three edges, anduser.createNotificationskips push for silent recipients
Observability coverage: 5 of 5 changes observable
| Change | Kind | Observable | Evidence |
|---|---|---|---|
pushDevice.register / pushDevice.unregister RPC |
inbound entrypoint | yes | Routed through the existing rpc dispatch; ObjectEffects.deliver span carries effect.kind/effect.target |
ApnsClient.send APNs request |
outbound HTTP call | yes | apns.ts:206 annotates peer.service: apns and server.address; the token-bearing URL is never recorded |
push.send outbox effect |
background work | yes | UserObject.sendPush span plus push.decision/push.skipped/push.outcome attributes |
directory.claimPushToken / user.forgetPushDevice |
background work | yes | Directory.claimPushToken and UserObject.forgetPushDevice spans with push.claim/push.forgotten |
loadPushContent / OrgEffects.messageDeleted |
background work | yes | OrgObject.pushContent and OrgEffects.messageDeleted spans, the latter with notification.recipients |
387efb7 · Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.
Loading