Skip to main content
RapidDev - Software Development Agency
App Featuresnotifications-alerts23 min read

How to Add Push Notifications to Your App (Copy-Paste Prompts Included)

Push notifications need four pieces: a permission request flow that explains the value before the system dialog, an FCM token registration that stores the token per device in Supabase, a notification sender function (Supabase Edge Function or Vercel API route calling firebase-admin), and a deep-link handler that routes the notification tap to the correct screen. With FlutterFlow the mobile path takes 3–4 hours for $0/month — FCM delivery is free at any volume. Web push adds a service worker step and is blocked on iOS Safari until the site is added to the Home Screen.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

notifications-alerts

Build with AI

3–6 hours with Lovable, V0, or FlutterFlow

Custom build

1–2 weeks custom dev

Running cost

$0/mo FCM delivery · $25/mo Supabase Pro at scale

Works on

WebMobile

Everything it takes to ship Push Notifications — parts, prompts, and real costs.

TL;DR

Push notifications need four pieces: a permission request flow that explains the value before the system dialog, an FCM token registration that stores the token per device in Supabase, a notification sender function (Supabase Edge Function or Vercel API route calling firebase-admin), and a deep-link handler that routes the notification tap to the correct screen. With FlutterFlow the mobile path takes 3–4 hours for $0/month — FCM delivery is free at any volume. Web push adds a service worker step and is blocked on iOS Safari until the site is added to the Home Screen.

What Push Notifications Actually Are

Push notifications are server-initiated messages delivered to a user's device even when the app is not open — a new message, a price drop, a reminder, an order update. On mobile, Firebase Cloud Messaging (FCM) is the universal delivery layer; it works on both iOS (via APNs) and Android at no per-message cost. On web, the W3C Push API with VAPID keys delivers to desktop browsers and iOS Safari PWAs (iOS 16.4+). The product decisions are: which categories of notifications to send (and let users opt out per category), how to handle the permission request so users say yes rather than instinctively blocking, and what happens when a notification tap should open a specific screen.

What users consider table stakes in 2026

  • Permission request dialog is triggered by a user action (button click, specific user intent), never on initial page load — cold permission requests are blocked by users at a much higher rate
  • An in-app explanation screen appears before the system permission dialog, describing what notifications the user will receive and why they are valuable
  • Notifications arrive on iOS and Android when the app is fully closed, not just backgrounded
  • Users can manage notification preferences per category (marketing, transactional, reminders) from a settings screen without having to go to OS settings
  • Notification includes an icon, title, body, and optional image; tapping it deep-links directly to the relevant content rather than just opening the home screen
  • Silent/data-only messages can trigger a background app refresh without showing a visible notification to the user

Anatomy of the Feature

Seven components across four layers. FlutterFlow handles the mobile path natively. The permission flow and the token lifecycle management are where first builds break on every platform.

Layers:UIDataBackendService

Permission Request Flow

UI

Browser: Notification.requestPermission() triggered by a button click, never on page load. Mobile Flutter: firebase_messaging.requestPermission() with alert, badge, and sound options enabled. Always shows an in-app explanation screen before the system dialog — explains what types of notifications the user will receive and gives them a meaningful reason to accept.

Note: The Notification API is blocked entirely inside Lovable preview iframes and V0 sandbox previews. Test permission flows only on the published URL or a real device.

FCM Registration

Service

Firebase Cloud Messaging is the delivery infrastructure. Mobile apps call FirebaseMessaging.instance.getToken() to retrieve an FCM registration token. Web apps call getToken({vapidKey}) from the firebase/messaging module inside a service worker context. Tokens are stored in the push_tokens table linked to user_id on every app open to keep them fresh.

Note: FCM tokens rotate when the user reinstalls the app, updates the OS, or clears app data. Always upsert the token on startup — do not assume the token stored at registration is still valid.

Push Tokens Table

Data

Supabase table storing one row per device per user. Columns: user_id, token (UNIQUE), platform (web/ios/android), created_at, last_active_at. Tokens are upserted on every app open using the UNIQUE constraint on token to avoid duplicates. The last_active_at timestamp helps identify and purge stale tokens that haven't been refreshed in 30+ days.

Note: One user can have many tokens across multiple devices. When sending a notification to a user, query all their active tokens and send to each — use FCM's multicast send API to batch up to 500 tokens per API call.

Notification Sender

Backend

Supabase Edge Function or Vercel API route that calls the Firebase Admin SDK (firebase-admin npm package). Accepts a payload (title, body, data, image_url, deep_link) and a user_id or list of tokens. Sends to individual tokens or FCM topics. Handles FCM 404 responses (invalid token) by deleting the token from push_tokens.

Note: Store the Firebase service account JSON as a Supabase Secret or Vercel environment variable, never in code. Parse it at runtime: JSON.parse(Deno.env.get('FIREBASE_SERVICE_ACCOUNT')).

Notification Preferences

Data

The user_notification_prefs table stores per-category opt-out preferences: user_id, category (marketing/transactional/reminders), is_enabled bool. The notification sender checks this table before sending to filter out opted-out users. Users update preferences from a settings screen without needing to go to OS settings.

Note: Check preferences server-side in the sender function, not client-side only — client-side filtering can be bypassed and doesn't protect users who have multiple devices.

Deep Link Handler

Backend

On notification tap, the app reads notification.data.screen and navigates to the correct page. Mobile: go_router or Navigator reads the screen value from message.data and pushes the target route. Web: the service worker's notificationclick event reads the notification action URL and calls clients.openWindow().

Note: Pass the target screen identifier in the data payload (not the notification body) — the data map is always delivered even for background/terminated notifications, while the notification body may be stripped by the OS.

Notification History

Data

The notifications_log table records every sent notification: id, user_id, title, body, category, sent_at, opened_at (nullable). opened_at is set when the user taps the notification and the deep link handler fires. This enables basic open-rate analytics without a third-party analytics service.

Note: Insert notifications_log rows from the sender function (sent_at), then update opened_at via an API call from the deep link handler when the notification is tapped.

The data model

Three tables cover token storage, notification preferences, and delivery history. Run this in the Supabase SQL editor — the service_role policy on notifications_log lets the sender function insert without exposing a public INSERT endpoint.

schema.sql
1create table public.push_tokens (
2 id uuid primary key default gen_random_uuid(),
3 user_id uuid references auth.users(id) on delete cascade not null,
4 token text not null unique,
5 platform text not null check (platform in ('web','ios','android')),
6 created_at timestamptz not null default now(),
7 last_active_at timestamptz not null default now()
8);
9
10create table public.user_notification_prefs (
11 user_id uuid references auth.users(id) on delete cascade primary key,
12 marketing bool not null default true,
13 transactional bool not null default true,
14 reminders bool not null default true
15);
16
17create table public.notifications_log (
18 id uuid primary key default gen_random_uuid(),
19 user_id uuid references auth.users(id) on delete cascade not null,
20 title text not null,
21 body text,
22 category text not null default 'transactional',
23 sent_at timestamptz not null default now(),
24 opened_at timestamptz
25);
26
27alter table public.push_tokens enable row level security;
28alter table public.user_notification_prefs enable row level security;
29alter table public.notifications_log enable row level security;
30
31create policy "Users manage own push tokens"
32 on public.push_tokens for all
33 using (auth.uid() = user_id)
34 with check (auth.uid() = user_id);
35
36create policy "Users manage own notification prefs"
37 on public.user_notification_prefs for all
38 using (auth.uid() = user_id)
39 with check (auth.uid() = user_id);
40
41create policy "Users view own notification log"
42 on public.notifications_log for select
43 using (auth.uid() = user_id);
44
45create policy "Service role inserts notification log"
46 on public.notifications_log for insert
47 with check (auth.role() = 'service_role');
48
49create policy "Service role updates notification log"
50 on public.notifications_log for update
51 using (auth.role() = 'service_role');
52
53create index push_tokens_user_idx
54 on public.push_tokens (user_id, last_active_at desc);
55
56create index notifications_log_user_sent_idx
57 on public.notifications_log (user_id, sent_at desc);

Heads up: The UNIQUE constraint on push_tokens.token ensures upserts on app startup never create duplicate token rows. last_active_at lets you write a weekly cleanup job that deletes tokens not refreshed in 60 days, keeping the table lean and preventing accumulation of stale registrations from uninstalled apps.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Native iOS + AndroidFit for this feature:

Best path for mobile push: FlutterFlow has a native Push Notifications panel under the Notifications section that handles FCM token registration, permission request, and basic send from Cloud Functions automatically. This is the fastest reliable mobile push implementation.

Step by step

  1. 1Connect your FlutterFlow project to Firebase (Authentication + Cloud Messaging): go to Settings → Firebase → Connect Firebase Project; FlutterFlow auto-generates google-services.json for Android
  2. 2Enable push notifications in FlutterFlow: Settings → Permissions → enable 'Push Notifications'; iOS NSUserNotificationDescription will be populated automatically with a default message — customize it to explain why your app sends notifications
  3. 3Add a Push Notifications action in the Action editor on the button or trigger that should request permission; select 'Request Permission' as the action type; FlutterFlow will show the system dialog with your customized message
  4. 4Store the FCM token: after permission grant, call FirebaseMessaging.instance.getToken() in a Custom Action and upsert the result to your push_tokens Supabase table with the platform set to 'ios' or 'android'
  5. 5Add deep link routing: in the App State, create a variable for pending_notification_screen; in the app's initState Custom Action, register a FirebaseMessaging.onMessageOpenedApp listener that sets this variable and triggers navigation
  6. 6Test on a real physical device — the FlutterFlow web preview and Run Mode do not deliver FCM push notifications; use the FlutterFlow mobile app on a real iPhone or Android phone

Where this path bites

  • FlutterFlow's built-in push panel uses FCM topic messaging for broadcast sends; for user-targeted push to specific tokens (send to this user's device), a Cloud Function that calls admin.messaging().send() with the specific token is needed
  • Limited UI control over the in-app permission explanation screen — must build a custom page before the system dialog rather than using built-in FlutterFlow components
  • iOS push requires both an APNs Auth Key in Firebase Console and the App Identifier configured with Push Notifications capability in the Apple Developer Portal — missing either causes silent delivery failure

Third-party services you'll need

FCM push delivery is free at any scale. The primary cost is infrastructure for the sender function and token storage as user count grows.

ServiceWhat it doesFree tierPaid from
Firebase Cloud MessagingPush delivery infrastructure for iOS (via APNs), Android, and web; handles token management and delivery confirmationFree for all push delivery at any volume; no per-message costFree; Cloud Functions on Blaze pay-as-you-go if using Firebase for the sender function (approx $0.40 per million invocations)
APNs (Apple Push Notification service)Apple's delivery layer for iOS push; required for any iOS push notifications via FCM; free from AppleFree from AppleFree; requires Apple Developer Program membership ($99/yr) for iOS app distribution
web-push npm packageOpen-source library for sending W3C Push API payloads from Node.js or Deno using VAPID keys; no per-message costOpen-source, zero costFree
Firebase Admin SDK (firebase-admin)Server-side npm package for sending FCM messages from a Supabase Edge Function or Vercel API routeOpen-source, zero costFree

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

FCM delivery is free. Supabase free tier handles 100 token rows and Edge Function calls. No additional service cost.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Push permission dialog never appears in Lovable or V0 preview

Symptom: The browser Notification API is blocked by browser security policy inside sandboxed iframes. Both the Lovable preview pane and the V0 sandbox run inside an iframe with a restrictive sandbox attribute that prevents the Notification API from functioning. Calling Notification.requestPermission() in a preview silently returns 'denied' or throws a security error, making it appear the code is broken when it is correct.

Fix: Test all push notification flows on the published URL or a Vercel preview deployment, never inside the embedded preview pane. In Lovable, click Publish and open the live URL. In V0, deploy to Vercel first. Document this expectation in your project notes so other team members don't waste time debugging in the preview.

iOS users don't receive push notifications after granting permission

Symptom: iOS web push only works when the website is installed as a Progressive Web App via 'Add to Home Screen' on iOS 16.4 or later. When a user visits the site in standard Safari and grants notification permission, the subscription appears to succeed and the token is stored — but no notifications are ever delivered. The failure is silent: FCM reports delivery success but the notification never appears on the device.

Fix: Add a prominent in-app prompt for iOS users: detect iOS via userAgent and show a step-by-step 'Add to Home Screen' guide before presenting the notification permission request. Add a manifest.json with display: standalone so the installed PWA launches without Safari chrome. Only request push permission after confirming the user is in standalone mode (window.matchMedia('(display-mode: standalone)').matches).

FCM tokens silently expire and notifications stop arriving for some users

Symptom: FCM tokens are not permanent. They rotate when a user reinstalls the app, clears app data, updates the OS, or when FCM proactively rotates them for security. The old token in push_tokens continues to exist in the database, and the sender function attempts delivery to it — FCM accepts the send request but returns a NOT_REGISTERED error in the response body (not an HTTP error code), which is easy to miss if error handling isn't implemented.

Fix: Call getToken() on every app startup and upsert the result into push_tokens with last_active_at updated to NOW(). In the sender function, parse the FCM multicast response and check for NOT_REGISTERED or INVALID_REGISTRATION error codes in the per-token results array; delete those specific token rows from push_tokens immediately. Add a weekly cleanup job that removes tokens where last_active_at < NOW() - INTERVAL '60 days'.

FlutterFlow push notifications work in debug but silently fail in release build

Symptom: Android FCM registration requires the SHA-1 fingerprint of the signing certificate registered in the Firebase Console. The debug build and the release build use different signing certificates with different SHA-1 fingerprints. FlutterFlow's test builds use the debug certificate; release builds uploaded to the Play Store use a different certificate. If only the debug SHA-1 is registered in Firebase, release builds get FCM tokens but delivery silently fails.

Fix: In the Firebase Console, go to Project Settings → Your Apps → Android app → Add Fingerprint. Add both the debug SHA-1 (from FlutterFlow's project settings) and the release SHA-1 (from your keystore or Google Play's app signing key, found in Play Console → Setup → App signing). Both fingerprints must be registered before release builds receive push notifications.

Best practices

1

Always show an in-app explanation screen before triggering the OS permission dialog — describe exactly what notifications the user will receive and why they are valuable; this doubles opt-in rates compared to showing the bare system dialog

2

Request permission only after a meaningful user action — a new message arrives, the user completes an order, or they explicitly visit notification settings; never on page load

3

Upsert FCM tokens on every app startup, not just at registration — tokens rotate and must be kept current; stale tokens cause silent delivery failures

4

Check user_notification_prefs server-side before every send — user preferences must be enforced at the infrastructure level, not just in the UI

5

Handle FCM's NOT_REGISTERED error code in the sender function and delete the stale token immediately — failing to do this causes the sender to accumulate invalid tokens and waste API calls

6

For multi-device users, send to all of their tokens in a single FCM multicast call (up to 500 tokens per call) rather than looping with individual sends

7

Log sent_at in notifications_log from the sender function and opened_at from the deep-link handler — even basic open-rate data reveals which notification categories users engage with versus which they ignore

When You Need Custom Development

FCM-based push via FlutterFlow or V0 covers the majority of notification use cases. These requirements go beyond what AI tools generate reliably:

  • Notification scheduling is required — sending at a specific time in the user's local timezone (e.g., 'send at 9 AM user's time') needs a queue-based delivery system rather than immediate send
  • Multi-channel fallback is required: push → SMS → email cascade when the previous channel fails or the user hasn't granted push permission
  • Per-notification conversion analytics are needed: tracking not just opens but downstream revenue or action attribution per notification campaign
  • GDPR/CCPA granular consent logging is required for each notification category with timestamp, IP, and user agent — needed for regulatory proof of consent in regulated markets

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

Do push notifications work on iPhone browsers?

Web push notifications work on iPhone only when the website is installed as a PWA (Progressive Web App) via Safari's 'Add to Home Screen' feature. This requires iOS 16.4 or later. Standard browser tabs in Safari, Chrome, or Firefox on iOS cannot receive push notifications. To maximize iOS reach, add an in-app prompt guiding iPhone users to add the site to their Home Screen before enabling notifications.

How do I avoid users blocking notifications?

Show an in-app explanation screen before triggering the system permission dialog. Describe exactly what notifications the user will receive — 'Get notified when your order ships, when a price drops, or when someone replies to you' — rather than showing the bare system dialog cold. Only request permission after a moment where notifications are clearly valuable (after placing an order, after setting a price alert). Research consistently shows that contextual permission requests get 2–3x higher acceptance rates than cold requests.

Can I send notifications to all users at once?

FCM supports topic messaging, where you subscribe tokens to a topic and broadcast a single send to the topic — this is how FlutterFlow's built-in push panel works. For more control, query all active tokens from push_tokens and batch them into FCM multicast calls (up to 500 tokens per call). For very large user bases (100K+), a fan-out queue (Supabase pgmq or Upstash Redis) is needed to avoid a single function call timing out while processing the full token list.

What's the difference between push notifications and in-app notifications?

Push notifications are delivered by the OS notification center when the app is closed — they appear on the lock screen and in the notification drawer. In-app notifications (real-time notifications) appear inside the app as a bell icon or a banner while the user is actively using the app, delivered via Supabase Realtime's WebSocket connection. Most apps use both: real-time for users who are active, push for users who have closed the app. They are complementary, not alternatives.

Do notifications cost money to send?

Firebase Cloud Messaging (FCM) delivery is free at any volume — there is no per-message cost. Apple Push Notification service (APNs) is also free. The only costs are your server infrastructure: a Supabase Edge Function or Vercel API route to call the FCM Admin SDK (Supabase free tier handles moderate volumes; Pro at $25/mo for larger scale). Web push is also free. The Apple Developer Program membership ($99/yr) is required to distribute iOS apps, but that's not a push notification cost specifically.

How do I handle users who have multiple devices?

Each device has its own FCM token, stored as a separate row in push_tokens. When sending a notification to a user, query all rows WHERE user_id = $userId and send to each token. FCM's sendEachForMulticast() API accepts up to 500 tokens per call, making multi-device delivery efficient. The UNIQUE constraint on the token column prevents duplicates if the same device registers multiple times (e.g., after app updates).

Can I schedule a notification for a specific time?

FCM does not support scheduled delivery — it sends immediately when you call the API. To send at a future time in the user's local timezone, store the notification in a scheduled_notifications table with scheduled_for timestamptz and a pg_cron job (Supabase Pro, $25/mo) that runs every minute to dispatch notifications where scheduled_for <= NOW() and sent_at IS NULL. Convert the user's preferred local time to UTC when inserting the scheduled_for timestamp.

What happens when a push token expires?

FCM returns a NOT_REGISTERED or INVALID_REGISTRATION error code in the send response (not an HTTP error — you must parse the response body). If you don't handle this, the stale token stays in push_tokens forever and the sender function keeps attempting delivery to it on every send. Handle it by parsing the FCM multicast response, finding failed tokens with these error codes, and deleting them from push_tokens immediately. Also call getToken() on every app startup to proactively refresh the token before it goes stale.

RapidDev

Need this feature production-ready?

RapidDev builds push notifications into real apps — auth, database, payments — at $13K–$25K.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.