Skip to content

feat(backend): APNs push notifications for iOS devices - #482

Merged
Makisuo merged 7 commits into
mainfrom
push/1-backend
Nov 14, 2026
Merged

Makisuo merged 7 commits into
mainfrom
push/1-backend

fix(backend): a push checks the token's owner and do not disturb when…

387efb7
Select commit
Loading
Failed to load commit list.
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/unregister store a token in the caller's own UserObject
  • schedulePush queues one push.send per device, deferred 60 s while the user is active
  • ApnsClient signs ES256 provider tokens and classifies each APNs refusal
  • push_device_tokens in D1 moves a token to its newest owner

Before merge

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: TracerDisabledWhen drops both the client span and header propagation (HttpClient.ts:569), and ApnsRequestError carries no cause
  • messageDeleted normalises Connect guest/host channel ids through notifiedChannel before fanning out, so no guest loop
  • notificationsRemoved is max(0, count−removed) on all three edges, and user.createNotification skips 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.