93796d882acaaaa9478ff3db3c652b7743f730c7
Step 4, and the last transport. Google directly via FCM HTTP v1 — no Expo, no firebase-admin. Unlike APNs the signed JWT is not the credential: it is an assertion exchanged at oauth2.googleapis.com for a 1-hour access token, so there are two things to cache and a network round trip on the cold path. Concurrent pushes share one in-flight exchange rather than each starting their own, and the token refreshes five minutes early so a request cannot race the expiry. The detail that breaks most first FCM integrations: `message.data` values must all be STRINGS. A number or boolean is rejected with a bare INVALID_ARGUMENT that does not say which field. Everything is stringified on the way out — id, ok and count included — and a test walks every value asserting its type rather than trusting the code that wrote it. Also v1-specific: there is no multicast (the /batch endpoint is deprecated), so N devices is N requests, which happens to match the APNs shape anyway. Errors are read for `error.status` only. FCM's `message` field can echo the device token, so logging the whole body would put device addresses in the logs. UNREGISTERED / INVALID_ARGUMENT / NOT_FOUND delete the row; anything else counts a strike. 21 tests across both channels, and verified against the real endpoint: a throwaway key gets 400 invalid_grant "account not found" from Google, meaning the endpoint, form encoding, grant_type, RS256 signature and claim structure were all accepted and only the account is missing. A malformed assertion would have failed earlier, with a different error. The doorbell rule is asserted on this channel too: a producer passing subject/from cannot get either into the serialized message. Still needed for a real send: FCM_SERVICE_ACCOUNT, a Firebase project, and google-services.json in the Android build. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Description
No description provided
42 MiB
Languages
TypeScript
90.9%
Shell
4.7%
JavaScript
4.1%
CSS
0.2%
HTML
0.1%