NukeBase

Push Notifications: Server API

Pointing at an area

The first argument to sendPush says where the devices are. It's a path, like every other data operation.

TargetWho gets it
["users", uid, "pushTokens"]Every device of one user.
["teams", teamId, "pushTokens"]Every device saved to that team.
["users", "$uid", "pushTokens"]Everyone. $ segments are wildcards, as in triggers and rules.
["fans"]Every device saved directly under fans.
[pathA, pathB]Several areas at once. A device found in both is sent to once.

What counts as a device

At each node you point at, the engine accepts one of two things:

  • A map of devices, like pushTokens below. This is what client.push.enable writes.
  • A single device: one subscription object with endpoint and keys.

It doesn't search any deeper. Pointing at ["users"] finds nothing, because the devices are two levels further down. Use the wildcard ["users", "$uid", "pushTokens"] instead. This keeps every send fast: the engine only looks where you told it to, however big the rest of your database is.

["users", uid] with two devices (values shortened)
"pushTokens": {
  "-u5r83BoVpsmbv4AmSUnmA": {                       // an Android phone (Chrome)
    "endpoint": "https://fcm.googleapis.com/fcm/send/cmJI…",
    "expirationTime": null,
    "keys": { "p256dh": "BDF4XAl5…", "auth": "-u5r83BoVpsmbv4AmSUnmA" }
  },
  "AA6TAGG9CDXjJHKtGKMTjg": {                       // an iPhone (Home Screen app)
    "endpoint": "https://web.push.apple.com/QKVLecOK…",
    "keys": { "p256dh": "BK2nrxlT…", "auth": "AA6TAGG9CDXjJHKtGKMTjg" }
  }
}

Each device is the exact object the browser produces (PushSubscription.toJSON()), stored unchanged and keyed by its auth value. That value is unique per device and stays the same when the device re-subscribes, so turning notifications on twice updates the same entry. If your app already stores subscriptions in this shape, sendPush can use them as they are.

Server API

FunctionReturnsDescription
sendPush(target, payload, opts?)Promise<Result>Sends to every device found at target (see Pointing at an area), 10 at a time.
configurePush({ publicKey, privateKey, contact })nothingOptional; call once at startup. publicKey and privateKey hardcode your own key pair (see Keys). contact is how Apple and Google can reach you about your messages, and defaults to your app's own address (https:// + DOMAIN). Most apps never call it.
Result
{
  sent: 2,      // accepted by the push service
  removed: 1,   // devices that are gone (uninstalled, permission revoked); deleted where they were found
  failed: 0     // temporary errors (push service down, rate limited); not retried automatically
}

Send options

OptionDefaultMeaning
ttl86400Seconds the push service keeps the message if the device is offline. Use a short value for time-sensitive messages ("your table is ready").
urgency"normal""very-low", "low", "normal" or "high". Phones in battery saver may hold back low-urgency messages.
topicnoneA short name (up to 32 letters and digits). A newer message with the same topic replaces an undelivered older one, for example "score".

The engine serves one route for the browser SDK: GET /push/key, your app's public key. There are no subscribe routes, because saving a device is an ordinary database write. To switch push off completely, set PUSH_DISABLED=1 in your .env and run nukebase env push.

The payload

FieldRequiredDescription
titleyesThe bold first line. Every push shows a notification, so sendPush rejects a payload without a title.
bodynoThe text under it. Phones show about 4 lines.
urlnoWhere tapping opens, as a path in your own app like "/orders/42". Links to other sites are ignored and open / instead.
iconnoA path to an image in public/. Defaults to the icon in your manifest.
tagnoA notification with the same tag replaces the one already on screen instead of stacking.
badgenoA number to show on the app's icon, like unread messages. 0 clears it. See App icon badge.
datanoAny small JSON your page may want when it opens.

The whole payload must be 3 KB or less once converted to JSON. sendPush rejects anything bigger with a clear error rather than letting the push service drop it. Send a short message and a url, and let the page load the details.

Rules

Saving a device is a normal write, so your rules decide where each device may be saved. Three common patterns:

server/rules.js
module.exports = {
  // 1. Each user saves their own devices
  "users": { "$uid": { "pushTokens": { "write": "$uid == admin.uid" } } },

  // 2. Team members save a device to their team
  "teams": { "$teamId": { "pushTokens": {
    "write": "root.teams[$teamId].members?.[admin.uid] === true"
  } } },

  // 3. No sign-in needed: anyone may add a device, but only something shaped like one
  "fans": { "$device": {
    "write": "true",
    "validate": "newData == null || (typeof newData.endpoint === 'string' && newData.endpoint.startsWith('https://') && newData.keys?.auth === $device)"
  } }
};

A stored device can't be misused. A subscription can't be used to send anything without your app's private key, which never leaves the server. Anything at a target that isn't shaped like a subscription is ignored, and a subscription with broken keys is removed on the next send. Still, keep devices private like any other user data: don't give browsers read access to everyone's records.

Only real push services are contacted. Devices are written by browsers, and the server sends to them, so the engine only posts to public https:// addresses. An endpoint on an IP address, on a local-only name, or on a name that resolves to a private network address is never contacted. IP and local-name entries are removed, and private-resolving ones are counted as failed. Real subscriptions always point at the browsers' push services (Apple, Google, Mozilla, Microsoft), so this never affects them.

Never let the browser choose the target of sendPush. A callable that sends to whatever path or uid the caller names lets anyone notify all your users. The server always decides the area, as in the examples below.

Keys

Every app has one VAPID key pair: a public key the browser subscribes with, and a private key that signs each message. You can let the engine make them, or bring your own.

Automatic (the default)

The first time push is used, the engine creates a key pair and stores it in your database at ["_nukebase", "pushKeys"]. Because they're part of your data, the keys are included in daily backups, nukebase backup and restore points, and they move with your data to another region.

_nukebase is ordinary data. Your rules decide who can reach it, like any other path, and rules deny by default, so browsers can't read it unless a rule grants it. Don't write a rule that does: anyone holding the private key and your saved devices could send notifications as your app. Your server code can read, set or remove it:

  • Setting a new { publicKey, privateKey } there switches keys at once, with no restart.
  • Removing it means a fresh key pair is made the next time push is used, which disconnects every subscribed device.

Your own keys (hardcoded in app.js)

To use keys you already have (from an existing app, web-push or Firebase), or to fix the generated ones in your code, pass them to configurePush near the top of server/app.js. Keys given here always take priority.

server/app.js
configurePush({
  publicKey:  "BPaste-your-65-byte-public-key-here…",   // 65 bytes, base64url
  privateKey: "paste-your-32-byte-private-key-here…",   // 32 bytes, base64url
  contact:    "mailto:hello@pawsbook.com"          // optional
});

When the engine creates keys automatically, the server log shows this exact line, ready to paste. A key that's malformed, or a public and private key that don't belong together, stops your app at startup with a clear message, instead of every send being refused later.

Keep your project's code private. Anyone with the private key and your saved devices could send notifications as your app. The devices are in your database, not your code, so the key alone isn't enough. Still, don't publish your code in a public repository.

Keys are for life. Changing them, or removing a configurePush key pair the app was already using, disconnects every subscribed device until each user turns notifications on again. The engine logs a warning at startup if your configured keys differ from the stored ones.

Local and hosted apps. A subscription belongs to one web address, so testing on localhost never reaches your real users, whichever keys you use.

Changing address means users turn notifications on again. Moving to a new domain, or nukebase regiontransfer to another region without your custom domain, gives your app a new web address, and existing subscriptions don't follow it. Your app's status() shows "off" for those users.

Limits & delivery

  • Payload: 3 KB of JSON at most.
  • Delivery isn't guaranteed. Phones in low-power mode, Focus modes or with notifications turned off may delay or drop messages silently. The push service only confirms that it accepted a message.
  • Offline devices receive the message when they reconnect, if ttl hasn't run out.
  • Speed: sends go out 10 at a time. Roughly 1,000 devices take a few seconds, and it doesn't block your app while it runs.
  • Wildcards look only at the exact shape you give, so ["users", "$uid", "pushTokens"] reads each user's pushTokens and nothing else of theirs.
  • Cost: free to send. Each message is a few KB of outgoing traffic, metered like any other traffic.
↑