Push Notifications: Examples & Troubleshooting
Examples
Notify when something is created (DB trigger)
New repair request: tell the landlord
addDbTrigger("set", ["repairs", "$id"], (context) => {
if (context.dataBefore !== undefined) return; // only brand-new requests
const repair = context.dataAfter;
const landlord = get(["buildings", repair.building, "owner"]).data;
sendPush(["users", landlord, "pushTokens"], {
title: `New repair: ${repair.flat}`,
body: repair.summary,
url: `/repairs/${context.path[1]}`,
tag: `repair-${context.path[1]}`
});
});A whole team
Team run moved: tell everyone on the team
addDbTrigger("update", ["teams", "$teamId", "nextRun"], (context) => {
const teamId = context.path[1];
sendPush(["teams", teamId, "pushTokens"], {
title: "Saturday's run has changed",
body: `Now ${context.dataAfter.time} at ${context.dataAfter.place}`,
url: `/teams/${teamId}`,
topic: "next-run" // a newer change replaces an undelivered older one
});
});Reminders at a set time
There's no built-in scheduler. Store the time on the record and check every minute. Mark each reminder as sent before sending, so a restart can never send it twice.
Booking reminders, 24 hours ahead
setInterval(() => {
const soon = Date.now() + 24 * 60 * 60 * 1000;
const due = query({ path: ["bookings"], query: `child.at <= ${soon} && !child.reminded` }).data || {};
for (const [id, b] of Object.entries(due)) {
update(["bookings", id], { reminded: true }); // mark first: never send twice
sendPush(["users", b.uid, "pushTokens"], {
title: "Appointment tomorrow",
body: `${new Date(b.at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} with ${b.groomer}`,
url: `/bookings/${id}`
});
}
}, 60 * 1000);Staff notify a customer (callable)
"Your order is ready" button for staff
addCallable("orderReady", async (data, admin) => {
if (!admin.claims?.staff) return { status: "Failed", message: "Staff only" };
const order = get(["orders", data.orderId]).data;
if (!order) return { status: "Failed", message: "No such order" };
update(["orders", data.orderId], { status: "ready" });
// The server picks the target from the order, never from the caller.
return sendPush(["users", order.customerUid, "pushTokens"], { title: "Your order is ready", url: `/orders/${data.orderId}` });
});Everyone, or fans without accounts
New show announced
// Browser, no sign-in (uses rule 3 above):
await client.push.enable(["fans"]);
// Server: every fan device, plus every signed-in user
addDbTrigger("set", ["shows", "$id"], (context) => {
if (context.dataBefore !== undefined) return;
sendPush([["fans"], ["users", "$uid", "pushTokens"]], {
title: "New show announced",
body: context.dataAfter.venue,
url: "/shows"
});
});Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The button does nothing on iPhone | The page is open in Safari, not installed. | Show the "install-first" hint: Share, then Add to Home Screen. |
enable() returns "not-saved" | Your rules refused the write, often because the user isn't signed in. | Check the write rule for that path. |
sendPush returns sent: 0 | The target points one level too high, for example ["users"]. | Point at the map of devices itself, or use a wildcard: ["users", "$uid", "pushTokens"]. |
status() is "unsupported" on a phone | The link was opened inside Facebook or Instagram. | Ask the user to open the page in Safari or Chrome. |
sent: 1 but nothing appears | Notifications are off for the app in the phone's settings, or Focus mode is on. | Check Settings, Notifications, then your app. |
| Everyone had to turn notifications on again | The keys changed (a different or removed configurePush pair, or _nukebase/pushKeys replaced or removed), /sw.js moved, or the app's address changed. | Keep all three stable. A lost key pair can only be recovered from a backup. |
sendPush rejects "payload too large" | Over 3 KB of JSON. | Send a short message plus a url. |