Push is the feature that usually makes founders give up on the PWA route and wrap their Lovable app properly. A web app on iPhone only gets push after the user adds it to the home screen, and almost nobody does. Wrap the same React code in Capacitor and you get real push notifications on iOS and Android. Getting Capacitor push notifications working takes an afternoon if you know the traps, and about three days if you don't.
Here's the version with the traps. (If your reminder only depends on a time the app already knows, like a booking, skip the server entirely and use Capacitor local notifications.)
The token you get on iOS is not the token you think
The official plugin is @capacitor/push-notifications (v8 at the time of writing). You call register(), you listen for the registration event, and you get a token. On Android that's a Firebase Cloud Messaging token. On iOS it's a raw APNs token.
That difference wrecks people. Your backend sends through FCM because that's what every tutorial uses, the Android test works on the first try, and every iOS send comes back with an invalid registration error. FCM can't do anything with an APNs token.
You have two ways out. Send iOS notifications straight to APNs and Android through FCM, which means two sending paths, two sets of credentials and twice the debugging. Or swap the official plugin for Capawesome's @capacitor-firebase/messaging, which pulls in the Firebase iOS SDK and hands you an FCM token on both platforms. We use the second one on almost every Lovable app we ship. One token format, one API call, one table. The cost is a heavier iOS build with Firebase pods in it, and I'll take that trade every time.
Apple's side
You need a paid Apple Developer account anyway, so: open Certificates, Identifiers & Profiles, find your App ID, and tick Push Notifications. Then go to Keys, create a key with Apple Push Notifications service enabled, and download the .p8 file. You can download it exactly once. Put it in your password manager before you do anything else.
In Firebase, open Project settings, then Cloud Messaging, and upload that .p8 with its Key ID and your Team ID.
Use the key, not a certificate. Firebase still accepts the older .p12 push certificates, and they're split into development and production. That split causes the classic bug. We took over an app from a founder whose push worked perfectly when they ran it from Xcode and never once arrived on TestFlight. They had uploaded a development certificate. Debug builds from Xcode talk to the APNs sandbox, TestFlight and App Store builds talk to production APNs, and their production slot was empty. A .p8 key works for both, doesn't expire every year, and makes the whole category of problem go away.
In Xcode, add the Push Notifications capability under Signing & Capabilities, and make sure AppDelegate.swift forwards didRegisterForRemoteNotificationsWithDeviceToken and didFailToRegisterForRemoteNotificationsWithError to Capacitor. The plugin docs have the exact snippet. If you skip it, register() resolves and the registration event simply never fires, with no error anywhere. Lovely.
Test on a real iPhone. Don't burn an hour on the simulator.
Android's side
Shorter. Download google-services.json from Firebase and put it in android/app/. Then two things people forget.
Android 13 and later need the POST_NOTIFICATIONS runtime permission, so call checkPermissions() and requestPermissions() before register(). Older tutorials skip this because they were written for Android 12, and on a Pixel 8 you get a token, a successful send, and nothing on screen.
And create a notification channel with createChannel() if you want control over importance and sound. Android 8+ files every notification under a channel, and the user can mute channels separately, so give "Order updates" and "Marketing" different channels from day one. Users who can mute your promos will keep your order updates on.
Sending from a Lovable backend
Your Lovable app already has a Supabase backend (Lovable Cloud or your own project), so the natural home for sending is an Edge Function.
Make a device_tokens table with user_id, token, platform and updated_at, with RLS so users can only insert and delete their own rows. Upsert the token every time the app starts, because tokens change after reinstalls and restores. The Edge Function signs a short-lived OAuth token with your Firebase service account and calls the FCM HTTP v1 endpoint. The old FCM "server key" API is gone, so any snippet that sends an Authorization: key=... header is out of date. Delete it.
When FCM answers a send with UNREGISTERED, delete that token row. Skip this and after six months half your table is dead tokens and your send logs are a wall of errors you've trained yourself to ignore.
Keep the service account JSON in Edge Function secrets. I've seen it pasted into a Lovable chat prompt, which puts it in the project history. Don't.
Things the plugin won't do
It doesn't support iOS silent push (background "content-available" pushes that wake the app). If you need background sync triggered by the server, that's native code or a different plugin.
On Android, data-only messages don't reach your pushNotificationReceived listener if the app was killed. Send a notification payload for anything the user must see.
When the app is open in the foreground, iOS shows nothing by default. Set presentationOptions in capacitor.config.ts to something like ["badge", "sound", "alert"] or your users will miss every push that arrives while they're using the app, which is exactly when your chat feature needs them.
Ask at the right moment
iOS gives you one system permission prompt. If the user taps "Don't Allow", the only way back is Settings, and nobody goes to Settings.
So don't ask on first launch. One booking app we built asked on the splash screen and roughly a third of users said yes. We moved the prompt to right after someone booked their first appointment, with a plain line above it: "Want a reminder the day before?" Most people said yes, because now the notification was obviously for them. Same app, same users, the prompt just showed up when it made sense.
Push is usually the first native feature we add when we turn a Lovable app into a real iOS and Android app, and it's a big part of why native beats a PWA for most products. If you're still choosing a wrapper, read Capacitor vs React Native first. Or tell us about your app and we'll wire it up for you.
Originally published on lovable2app.com. Aakash Verma, software developer and founder of Lovable 2 Live.
Top comments (0)