APNs + FCM + Web Push + Live Activities + Geofences + Critical Alerts + per-org frequency caps + quiet hours + sticky-by-user A/B — one SDK, three transports.
OneSignal and Braze send pushes. Sendora's Push module ships the same transport surface (APNs cert + token, FCM, Web Push VAPID), plus Live Activities (iOS ActivityKit + Android ongoing notifications) + Geofences + Critical Alerts + per-org frequency caps + TZ-aware quiet hours + sticky A/B variant assignment + automatic invalid-token pruning. Device-takeover on signin retires the anon `user_id` so a single device doesn't get duplicate pushes. Honest about scope: audience fan-out is via Automation workflows (not a single-call `sendToAudience()` API), and the A/B test ships variant assignment + manual winner declaration — not automatic statistical readout or auto-promote.
Features
- APNs + FCM + Web Push (VAPID) — one SDK, one send API, three transports. Cert + token auth on APNs. Lazy-generated VAPID keypair per-org on first web subscription.
- Live Activities (iOS ActivityKit) + Live Updates (Android ongoing notifications) — push-to-update lock-screen widgets. Cross-platform via
pushLiveActivities.platformdiscriminator. - Geofences (iOS + Android) — server-defined circular regions; SDK reports enter / exit / dwell back; backend writes
geofence.<event>rows that Automation workflows can subscribe to. - Critical Alerts (iOS) —
aps.sound={critical:1,name,volume}+interruption-level=criticalbypasses DND / Focus / silent (with the Apple entitlement + user permission). - Device-takeover on signIn — anon → identified flip retires the anon
user_id, reassigns push tokens, hard-deletes the anon row, emitsauth.device_takeover. No duplicate pushes after login. - Per-org delivery policies —
freqCapPerUserPerDay/freqCapPerUserPerHour(counts non-suppressed rows in window).quietHoursStartLocal/quietHoursEndLocal(0-23) evaluated against the recipient'spushTokens.timezone. Wraps midnight.deferInQuietHours→ schedule for window end; otherwise → suppress with reason. - Sticky-by-user A/B test —
assignAbVariant(test.id, userId)hashes to a uniform 0-99 bucket; same user always lands on the same variant. Honest: manual winner declaration viasetPushAbTestStatus(winnerKey). No automatic statistical readout, no auto-promote. - Templates + localization —
localized_body: { "en": ..., "fr": ... }resolved per-recipient viapushTokens.locale(exact match → 2-letter fallback → default). - Rich payloads + action buttons —
actions: [{ id, title, url? }](max 4, APNs limit). Body click →push.opened; action click →push.clickedw/ action id. - Open + click tracking — SDK fires
POST /push/track-openon tap; backend stampspushSends.openedAt + clickedAt + clickAction+ emits analytics event. - Invalid-token pruning — dispatch errors matching
UNREGISTERED|BadDeviceToken|...flippushTokens.isActive=false+ emitpush.token_invalidated. Hourly GC cron deletes inactive tokens after 30-day grace. - Honest non-features: no single-call
sendToAudience()(fan-out via Automation workflow); no automatic Deep-Link auto-wrap on push payloads for ROAS (customer provides URL); nosilentpush convenience param exposed (build via raw payload); A/B test has no statistical-readout engine.
Common use cases
Replace OneSignal ($139/mo at 10K mobile MAU) + per-platform geofence vendors + Live Activity gateways with one tenant.
Multi-platform mobile apps that need APNs + FCM + Live Activities + Live Updates without four separate vendors.
Lifecycle messaging that survives signin — device-takeover collapses anon + identified to one identity, so no duplicate-push storm post-login.
Start in minutes. Scale without switching tools.
The free tier covers most side projects. Every module is turn-key and every SDK is first-party.