FCM Push from Apps Script: JWT Encoding and Recipient Management
TECH INSIGHT / EXPLAINED
Push delivery involves two different tokens. An OAuth access token authorizes the server to send; an FCM registration token identifies a receiving app instance. Keeping them separate makes authentication failures and recipient failures easier to handle correctly.
Local execution of the actual project
These captures show the existing project UI or functions executed locally with non-private sample inputs. Each caption states the scope of the execution.
Calling HTTP v1 from Apps Script
Apps Script can call FCM HTTP v1 through UrlFetchApp. Signing a service-account JWT manually is one implementation option. If a maintained authentication library or a separate sending service is available, compare its maintenance burden with manual cryptographic handling.
The service-account flow creates an RS256 header and claims, exchanges the signed JWT at https://oauth2.googleapis.com/token, and obtains a short-lived access token. Use the https://www.googleapis.com/auth/firebase.messaging scope and the token endpoint as aud. iat and exp use seconds; exp must be no more than one hour after iat. The scope and appropriate IAM permission on the target project are both necessary.
Remove Base64url padding
Encode UTF-8 bytes with URL-safe Base64 and remove trailing = padding for each JWT segment. This fragment assumes that a claims object has already been prepared. It constructs signing input without using a key or sending a request.
function base64urlUtf8(text) {
return Utilities.base64EncodeWebSafe(
text, Utilities.Charset.UTF_8
).replace(/=+$/, '');
}
// JWT signing input only; no key or network request here.
const headerPart = base64urlUtf8(JSON.stringify({alg: 'RS256', typ: 'JWT'}));
const claimPart = base64urlUtf8(JSON.stringify(claims));
const signingInput = headerPart + '.' + claimPart;
Sign the exact input bytes and append the signature bytes encoded as unpadded Base64url. Check the token exchange's HTTP status, JSON errors, access_token, and expires_in. Cache with an expiry margin and coordinate simultaneous refreshes with a lock. Never print complete JWTs or tokens in debugging logs.
Key storage is separate from encoding
Correctly parsing the service-account JSON restores escaped newlines in private_key. Base64 wrapping can simplify a storage path, but it is neither encryption nor a guarantee against corruption. If using Script Properties, assume project editors can access the key. Restrict access, plan rotation and revocation, and choose secret management appropriate to the deployment.
Register recipients through authenticated requests
Send the client SDK's registration token through an authenticated POST from the signed-in user. URL query parameters can persist in history and logs. Store the user, app instance, token, and refresh time together; clear associations on logout or unlinking. Do not overwrite a multi-device user's registrations with one token.
Check the actual execution principal and service user instead of relying solely on User Properties scope. Never confuse the server's OAuth token with a client registration token or return server credentials to the browser.
Accepted does not mean displayed
The send endpoint is https://fcm.googleapis.com/v1/projects/PROJECT_ID/messages:send. muteHttpExceptions lets code inspect error HTTP responses; it does not turn them into success. Remove a registration on UNREGISTERED, investigate authentication and permission errors, and use delayed retries for transient and quota errors according to official guidance.
A returned message ID does not confirm display on a device. Keep important events in the application's own history with event_id so they can be retrieved. High priority does not guarantee immediate delivery. Minimize sensitive account information on lock screens. Even with Telegram as another path, a shared backend failure can stop both channels.
Recipient linking and delivery queue design
References
Content and examples reviewed: 2026-09-29
댓글
댓글 쓰기