NukeBase

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

SymptomLikely causeFix
The button does nothing on iPhoneThe 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: 0The 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 phoneThe link was opened inside Facebook or Instagram.Ask the user to open the page in Safari or Chrome.
sent: 1 but nothing appearsNotifications 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 againThe 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.
↑