Base44 Push Notifications with OneSignal (Native)
Base44 push notifications, done natively: full OneSignal, Apple, and Firebase setup with Despia, then link users and send from your backend.

Base44 builds the app, but a web app cannot deliver a push to a phone that is closed, and on iOS web push barely works at all. Despia includes the native OneSignal SDK in your iOS and Android binaries, so your Base44 app sends real notifications through Apple and Google, delivered even when the app is not running. This is the whole setup: OneSignal, Apple, Firebase, and Despia, then linking users and sending.
Why web push does not cut it on a mobile app
Web push has two hard limits on a real mobile app. On iOS it only fires for a PWA the user manually added to the home screen, it did not exist before iOS 16.4, and it is unreliable even now. And a web page receives nothing once the browser or app is fully closed, which is exactly the moment a notification earns its keep. Native push goes through APNs on iOS and FCM on Android, wakes the device, and lands on the lock screen whether or not your app is open. Despia compiles OneSignal's native SDK into the binary, so you get that delivery while your Base44 app stays the single codebase.
The full setup: OneSignal, Apple, Firebase, Despia
Most of this is one-time credential wiring. Do it in order, each step feeds the next.
Create the OneSignal app
Sign up at onesignal.com and create an app. It is free up to 10,000 subscribers, so it does not gate early work.
When it asks for platforms, pick Native iOS and Native Android. Do not choose Web Push, even though your Base44 code is web. Despia ships a native binary, so it needs the native platforms.
iOS: Apple Push Key and bundle IDs
In Apple Developer, go to Certificates, Identifiers and Profiles, then Keys, and create a key with Apple Push Notifications service (APNs) enabled. Download the
.p8(you get one download), and note its Key ID plus your Team ID from the top right of the account.Under Identifiers, open your core bundle id (for example
com.despia.myapp) and enable the Push Notifications capability. Apple rejects registration without it, even with a valid key.Create a second bundle id for OneSignal's notification service extension: your core id with
.OneSignalNotificationServiceExtensionappended, socom.despia.myapp.OneSignalNotificationServiceExtension. Enable Associated Domains and Push Notifications on it. The name must be exact, Despia provisions this target at build time.Optional, for delivery metrics or rich push with images and buttons: create an App Group
group.com.despia.myapp.onesignaland add it to both bundle ids. Basic push works without it.Back in OneSignal, open Settings, Push and In-App, Apple iOS. Upload the
.p8, paste the Key ID, Team ID, and iOS bundle id, and save. OneSignal validates with Apple immediately.
Android: Firebase
Create a project at console.firebase.google.com and add an Android app with the same package name as your Despia app. In Project settings, Cloud Messaging, grab the Server Key and Sender ID. If the legacy Server Key is hidden, enable the Cloud Messaging API in Google Cloud first.
In OneSignal, open Settings, Push and In-App, Google Android, paste the Server Key and Sender ID, and save.
Keys and Despia
In OneSignal, open Settings, Keys and IDs. Copy the OneSignal App ID for the client, and the REST API Key for your backend. The REST API Key stays on your server, never in client code.
In the Despia Editor, open App, Settings, Integrations, OneSignal, add your OneSignal App ID in the iOS and Android fields (the same value in most setups), and save. Then trigger a fresh build. The OneSignal SDK and the notification service extension compile into the binary and are signed against the bundle ids you registered, so this cannot ship over the air.
Until you rebuild, OneSignal stays inactive even with the App ID saved. The setonesignalplayerid:// call resolves silently, and backend sends return success from the API but never reach the device. If notifications stop after a settings change, rebuild before anything else.
Link the device to your user
Despia registers the device with OneSignal at launch. Your job is to tell OneSignal which user this device belongs to, on every authenticated load, so you can target that user later by your own id.
import despia from 'despia-native'
const isDespia = navigator.userAgent.toLowerCase().includes('despia')
// link this device to your user on every authenticated load
function linkPush(userId) {
if (!isDespia) return
despia(`setonesignalplayerid://?user_id=${userId}`)
}
The user_id you pass here becomes the user's external_id in OneSignal, and it is the exact value you target with include_external_user_ids when you send. Keep it stable, and keep it identical on both ends, or the send reaches no one.
Ask for permission without nagging
Check whether push is actually enabled and, if not, send the user to their settings instead of blocking them.
async function promptIfPushOff() {
if (!isDespia) return
const { nativePushEnabled } = await despia('checkNativePushPermissions://', ['nativePushEnabled'])
if (!nativePushEnabled) despia('settingsapp://') // opens this app's system settings
}
A quiet banner on load when push is off converts better than a hard gate.
Send from a Base44 function
Sends run server-side so the REST API key stays private, and Base44 can run backend functions, which is where this belongs. Target the same id you linked.
// server-side in a Base44 function, REST API key never ships to the client
async function sendPush(userId, title, message) {
await fetch('https://onesignal.com/api/v1/notifications', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${ONESIGNAL_REST_API_KEY}`,
},
body: JSON.stringify({
app_id: ONESIGNAL_APP_ID,
include_external_user_ids: [userId],
headings: { en: title },
contents: { en: message },
}),
})
}
Open the right screen when they tap
A notification is only useful if the tap lands the user somewhere specific. OneSignal carries a data object with each notification, and Despia reads three fields from it on tap. path updates the URL through the History API and fires a popstate, with no reload, which is what you want almost always. metadata is any JSON you attach to restore state. url is the legacy option that forces a full WebView reload, for when a reload is genuinely needed.
Send the deep link as part of the same backend call:
// server-side in a Base44 function, send with a deep link
async function sendPush(userId, title, message, path, metadata) {
await fetch('https://onesignal.com/api/v1/notifications', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${ONESIGNAL_REST_API_KEY}`,
},
body: JSON.stringify({
app_id: ONESIGNAL_APP_ID,
include_external_user_ids: [userId],
headings: { en: title },
contents: { en: message },
data: { path, metadata },
}),
})
}
// deep link a user to their shipped order
sendPush(userId, 'Your order shipped', 'Tap to track it', '/orders/4567?tab=tracking', { orderId: 4567 })
That is all path needs. Despia updates the URL and fires popstate, and your router navigates on its own, so there is no client code for the common case. Add window.onNotificationEvent only when you send metadata and want to restore state, assigned outside the isDespia gate:
window.onNotificationEvent = (payload) => {
if (!payload.metadata) return
// object from the REST API, string from the OneSignal dashboard
const meta = typeof payload.metadata === 'string' ? JSON.parse(payload.metadata) : payload.metadata
restoreState(meta)
}
The one exception is a router that ignores the synthetic popstate. If that is yours, navigate from payload.path in the same handler instead, but do not also lean on the automatic change, or you navigate twice.
The tap resolves from any app state: foreground changes the path immediately, background applies it once the WebView is shown, and a cold start buffers the payload until the page loads. It fires once per tap.
Get it on the stores
Take your Base44 app to iOS and Android with native push already wired in. Code signing and submission run from the browser, no Mac and no CLI.





