How to Add Push Notifications to an AI-Built App
A real technical walkthrough for adding push notifications to an app you built with AI, covering web push with VAPID keys and service workers, native push tradeoffs, and the permission prompt UX that decides whether anyone opts in.
Push notifications are one of the few growth channels an early-stage app can control without buying attention or fighting an algorithm. If you are figuring out how to add push notifications to an AI-built app, the short version is this: use web push, a service worker plus the Push API, unless you specifically need iOS-native reliability, rich media, or App Store distribution, in which case you route through APNs and FCM instead. Both paths share the same core idea, a subscription endpoint that the browser or OS hands you, and a server that pushes messages to it whenever something worth interrupting the user has happened. The rest of this covers the real mechanics, including the permission prompt UX that decides whether anyone opts in at all.
Web Push vs Native Push: Which One Your App Actually Needs
Most apps someone built with an AI app builder or a coding agent start life as a web app, or a web app wrapped in something like Capacitor. That matters here, because it determines whether web push alone covers you or whether you also need to stand up native push for an app store release.
Web push runs entirely in the browser. A service worker sits between your app and the network, and it can receive a push event and show a system notification even when your tab is closed. No app store review, no separate mobile codebase, and it works on desktop and Android out of the box. The catch is iOS: Safari only delivers web push to apps the user has explicitly added to the home screen, a limitation in place since iOS 16.4.
Native push means talking to Apple Push Notification service (APNs) directly, or, more commonly, going through Firebase Cloud Messaging (FCM), which wraps APNs and Android's delivery system behind one API. This is the only reliable path if you are shipping a native or React Native app through the App Store, and it gives you things web push does not: guaranteed delivery even when the OS has been aggressive about killing background processes, richer media in the notification itself, and badge counts on the app icon.
Web push | Native push (APNs/FCM) | |
|---|---|---|
Setup effort | Service worker plus VAPID keys, hours | Platform SDK plus certificates or server keys, a day or more |
Platform coverage | Desktop browsers, Android, installed iOS home-screen apps | iOS App Store, Android |
App store review | Not required | Usually required for the app shipping it |
Rich media, badges | Limited | Full support |
Why Asking for Permission on Page Load Kills Opt-In Rates
The browser's permission dialog is a one-shot event. If a user hits block, most browsers will not let your site ask again. The button just silently does nothing until the person manually finds your site in browser settings and resets it. That makes the timing of the first ask more important than almost any copy you put around it.
The most common mistake is firing `Notification.requestPermission()` the moment the page loads, before the user has any idea what the app does or why it would notify them. web.dev's guidance on this is blunt: the worst thing you can do is show the permission dialog as soon as users land on your site, because they have no context for why they should say yes, and a reflexive block costs you that user permanently.
The fix is a two-step ask. Show your own UI first, a banner, a modal, or a toggle tied to something the user just did, that explains what they will get and why. Only trigger the real browser prompt after they click through that. Good moments to attach it to include:
Right after a user completes an action tied to the notification, like placing an order or finishing a setup step
When they opt into something ongoing, like following a topic or starting a long-running job
From a settings panel or bell icon they choose to click, rather than a prompt that appears on its own
None of this is about tricking people into a yes. It is about only asking when the value is obvious, which is also just better product design once you have added user accounts to your AI-built app and can tie notification preferences to a real profile instead of an anonymous browser session.
How to Add Web Push Notifications to Your App, Step by Step
This is a working walkthrough for a standard web app, not a native app. It assumes you already have a backend that can store data and make outbound HTTP requests, which is true of essentially anything generated by a modern AI app builder or coding agent.
1. Generate VAPID keys
VAPID, Voluntary Application Server Identification, is how your server proves to the push service, Chrome's endpoint, Mozilla's autopush, and so on, that it is the same server that created a given subscription, without ever handing over a shared secret. It is a public and private key pair. The public key goes to the browser, the private key stays on your server and signs every push you send.
The easiest way to generate a pair is with the `web-push` npm package:
npx web-push generate-vapid-keys
# or programmatically, inside a setup script:
# const webpush = require('web-push');
# const { publicKey, privateKey } = webpush.generateVAPIDKeys();Store the private key as a server-side environment variable. The public key is not sensitive, it gets sent to the browser as part of the subscription call below.
2. Register a service worker and wire up the permission prompt
Your service worker needs a `push` event listener that turns an incoming message into a visible notification, plus a `notificationclick` handler so tapping it does something:
// public/sw.js
self.addEventListener('push', (event) => {
const data = event.data ? event.data.json() : {};
event.waitUntil(
self.registration.showNotification(data.title || 'New update', {
body: data.body,
icon: '/icon-192.png',
data: { url: data.url || '/' }
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(clients.openWindow(event.notification.data.url));
});On the client, do not call this on page load. Attach it to a button or a moment that makes sense in your product, following the timing rules above:
async function subscribeToPush(currentUser) {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
});
await fetch('/api/push/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subscription, userId: currentUser.id })
});
}
document
.getElementById('enable-notifications')
.addEventListener('click', () => subscribeToPush(currentUser));The `applicationServerKey` argument is your VAPID public key, converted from base64 to the byte array format the Push API expects. That conversion function is a few lines of boilerplate that most web push tutorials include verbatim. If you are working with a coding agent, this is a fine thing to ask it to generate rather than hand-write.
3. Store the subscription on your server
The subscription object the browser returns is not a token, it is a small JSON payload containing an endpoint URL and two keys used for encryption. Store it against the user record it belongs to, the same table you would already have touched when you set up accounts, so you can look up every subscription for a user and every user subscribed to a given notification type without scanning everything.
If multiple devices or browsers subscribe for the same user, keep all of them. People check email on their phone and their laptop, and a stale subscription from a browser they no longer use should just fail silently rather than block the others.
4. Send a test notification
With the subscription stored and VAPID keys configured, sending a push is a single call from your server:
const webpush = require('web-push');
webpush.setVapidDetails(
'mailto:you@yourapp.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
async function sendTestPush(subscription) {
const payload = JSON.stringify({
title: 'Your export is ready',
body: 'Click to download the file.',
url: '/exports/latest'
});
await webpush.sendNotification(subscription, payload);
}If the push service returns a 404 or 410, the subscription has expired or the user revoked permission, and you should delete it from your database rather than retry it. That is normal churn, not a bug.
When Native Push Is Worth the Extra Setup
Reach for APNs or FCM instead of, or alongside, web push when any of these are true:
You are distributing through the iOS App Store or Google Play as a native or React Native build, not a browser tab or installed home-screen app
You need delivery to stay reliable even when iOS has aggressively suspended your app in the background
You want rich push, images, action buttons, or badge counts on the home screen icon
Your audience is disproportionately on iPhones outside your installed-PWA users, since the home-screen requirement for iOS web push is a real drop-off point
FCM is usually the pragmatic choice even for reaching iOS users, since it wraps APNs behind one API and one SDK instead of making you integrate with Apple's certificate-based system directly. The tradeoff is a dependency on Google's infrastructure and more setup than web push: platform credentials, a mobile SDK integration, and, for iOS, an Apple developer account and push certificate before you send a single notification.
Most apps built with AI tools do not need any of this on day one. Start with web push, see whether people actually engage with what you send, and only build the native path once you are shipping an actual App Store binary.
Testing, Debugging, and Making It Worth the Effort
Test in more than one browser before you trust it. Chrome, Firefox, and Safari all handle the permission prompt and service worker lifecycle slightly differently, and a subscription created in one is not portable to another.
Track what happens after you send, not just whether the send succeeded. A notification that gets delivered but never clicked is telling you something, and it is worth measuring the same way you would measure any other feature once you have added analytics to your AI-built app, rather than treating push as a fire-and-forget channel.
Keep the service worker itself lean. It runs outside your main app bundle, but it still executes on the user's device, and a bloated or poorly written one is a subtle way to end up chasing why your AI-built app feels slow months after you shipped it and forgot it was there.
Keep the expectations in scale, too. Push notifications are a retention and re-engagement lever, not a growth engine by themselves. They matter more once you have people to re-engage, which is a separate problem covered in getting your first 100 users for an AI app. If you have not built the app itself yet, that process is covered in how to build an app with AI, and push notifications are worth adding once you have something people are already opening regularly, not before.
FAQ
Do web push notifications work on iPhone?
Yes, since iOS 16.4, but only for web apps the user has explicitly added to the home screen through Safari's Share menu. An open Safari tab cannot receive web push, and there is no automatic install prompt on iOS, so users have to add the app manually.
What are VAPID keys used for?
VAPID keys let your server prove to the browser's push service that it is the same server that created a given subscription, without sharing a secret. The public key is sent to the browser during subscription, and the private key signs every push message your server sends.
Can I add push notifications without a backend server?
No. You need a server to store subscription objects and make the authenticated call to the push service whenever you want to send something. An AI app builder or coding agent can generate that scaffolding for you, but the persistent storage and send logic still has to live somewhere.
Why do users keep denying my notification permission prompt?
Usually because it appears before they understand what they would be opting into, most commonly on page load. Move the request to a moment tied to something the user just did, and use your own UI to explain the value before triggering the browser's dialog.
Is web push free to use?
Yes. Browser push services like Chrome's and Mozilla's do not charge per message. Your only costs are running the server that stores subscriptions and sends notifications, which is typically the same backend already running your app.
How did this land?
About the author

Developer Advocate
Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.


