Send push notifications from Node.js to your phone
Your background job ran. Your deployment finished. A user just signed up. You want to know immediately — not by polling a dashboard, not by ssh-ing into a server, but on your phone, right now.
Here are three ways to do it from Node.js. They’re not equal. Pick the one that matches what you actually need.
Option 1: Raw VAPID + the web-push library
The Web Push protocol is the standard underneath every browser notification. If you want full control — your own subscription storage, your own worker, no third-party account — this is the path.
It’s also real work.
You need a PWA with a registered service worker. The browser issues a push subscription (an endpoint + keys). You store that subscription server-side. Then you sign payloads with VAPID keys and deliver them via web-push.
import webpush from "web-push"
webpush.setVapidDetails(
"mailto:[email protected]",
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
)
// subscription comes from navigator.serviceWorker.pushManager.subscribe()
// you stored it in your DB when the user opted in
const subscription = await db.getPushSubscription(userId)
await webpush.sendNotification(
subscription,
JSON.stringify({ title: "User signed up", body: user.email })
)What you’re signing up for: generating VAPID keys, writing a service worker, handling permission prompts, storing subscriptions per device, pruning stale ones when they return 404 or 410, and building the frontend that subscribes. On iOS, this only works when the site is added to the home screen.
The tradeoff is real: maximum control, significant setup. No third party in the delivery path. Every subscription belongs to you.
Option 2: Firebase Cloud Messaging
FCM is Google’s push infrastructure. It supports Android natively, browsers via their web SDK, and iOS through APNs (which requires an Apple Developer account and a provisioning certificate).
Setup: create a Firebase project, download a service account JSON, add the Firebase SDK to your frontend, and initialize the Admin SDK in your backend.
import { initializeApp, cert } from "firebase-admin/app"
import { getMessaging } from "firebase-admin/messaging"
initializeApp({
credential: cert(require("./service-account.json")),
})
const messaging = getMessaging()
// token comes from getToken() in the Firebase JS SDK on the client
await messaging.send({
token: deviceToken,
notification: {
title: "User signed up",
body: user.email,
},
})What you’re signing up for: a Firebase project, a Google account dependency, a service account file that needs to be kept out of version control, and a frontend integration that registers for FCM tokens. The tokens are per-device and expire; you need to handle rotation.
FCM is mature and works well if you’re already in the Firebase ecosystem. If you’re not, you’re pulling in a significant dependency for a notification channel.
Option 3: trigger.fyi
This one is different in scope. It’s not for notifying your users — it’s for notifying yourself, the developer. Think: deployment alerts, error spikes, signups, job completions. Anything you’d otherwise grep logs for.
The model is simple: one key, one app, one endpoint. POST text to https://trigger.fyi/<key> and your phone buzzes. The npm package wraps that in a function call.
npm install trigger.fyiimport fyi from "trigger.fyi"
// TRIGGER_FYI_SECRET_KEY in your environment
fyi("User signed up", { body: user.email, userId: user.id })That’s it. No service worker. No subscription management. No Google account. No config files.
fyi() is fire-and-forget — it never throws, never blocks. If you’re on a serverless platform where un-awaited promises can be dropped at response end, await it:
await fyi("Payment processed", { amount: charge.amount })It returns a promise for those who want it, but your caller never pays for the latency either way.
There are three levels:
fyi("Signup") // normal push — shows on lock screen
fyi.log("Health check") // feed-only, no push, no notification sound
fyi.critical("DB down") // high-urgency push, requireInteraction where supportedOne honest limitation: fyi.critical() on iOS web push can’t break silent mode. iOS doesn’t allow web push to override the mute switch — that requires a native app. The docs say this clearly. On Android and desktop, critical behaves as expected.
The CLI (npx trigger.fyi) opens a TUI that watches your incoming events live. Sending is still curl:
curl -X POST https://trigger.fyi/your-key -d "Deployed to production"What you’re signing up for: nothing, initially. Run npx trigger.fyi, scan the QR code, and you have a key and a subscribed device. The first notification takes under a minute.
Which one
If you’re building a notification feature for end users — they subscribe, they get alerts — raw VAPID or FCM. VAPID if you want no third-party dependency. FCM if you need Android support or you’re already Firebase-native.
If you want your phone to buzz when something happens in your own code — a job finishes, a user signs up, an exception spikes — trigger.fyi is the fastest path. Three lines, no infrastructure, and it stays out of your hot path.