FlowAlert
All posts
How to Send Push Notifications from Next.js (App Router & Server Actions)
nextjsreactpush-notificationstutorial

How to Send Push Notifications from Next.js (App Router & Server Actions)

A practical guide to sending real-time push notifications from a Next.js application using Server Actions, Route Handlers, and the FlowAlert API — with TypeScript examples.

FlowAlert TeamMay 12, 2026 6 min read

How to Send Push Notifications from Next.js (App Router & Server Actions)

Push notifications from a Next.js app are something most guides get wrong. They either walk you through building a full WebSocket server, setting up Firebase Cloud Messaging boilerplate, or mucking around with service workers — all just to fire an alert to yourself when a user signs up.

This guide takes a different approach: use the FlowAlert REST API from your Next.js backend (Server Actions or Route Handlers) to send push notifications to any mobile device in under 10 lines of code.


When You Need This

  • User signed up → send yourself a push notification
  • New payment → immediate alert before the Stripe dashboard updates
  • Form submitted → get the lead on your phone while it's hot
  • Background job failed → wake you up before users notice
  • Any server-side event → notify the right person instantly

Setup

1. Install the SDK (optional — or just use fetch)

FlowAlert has a simple REST API. No SDK required:

npm install @flowalert/node # or just use native fetch — no package needed

2. Add your API key to environment variables

# .env.local FLOWALERT_API_KEY=fa_live_xxxxxxxxxxxx

Never commit this to git. Add it to your Vercel/Netlify environment variables for production.


The Core Utility Function

Create lib/notify.ts:

1const FLOWALERT_API = 'https://api.flowalert.io/v1/events'; 2 3type Priority = 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT'; 4 5interface NotifyOptions { 6 event: string; 7 title: string; 8 body: string; 9 priority?: Priority; 10 data?: Record<string, unknown>; 11 url?: string; 12} 13 14export async function notify(options: NotifyOptions): Promise<void> { 15 const apiKey = process.env.FLOWALERT_API_KEY; 16 if (!apiKey) return; // gracefully no-op if not configured 17 18 try { 19 await fetch(FLOWALERT_API, { 20 method: 'POST', 21 headers: { 22 'Content-Type': 'application/json', 23 'X-Api-Key': apiKey, 24 }, 25 body: JSON.stringify({ 26 event: options.event, 27 title: options.title, 28 body: options.body, 29 priority: options.priority ?? 'NORMAL', 30 data: options.data, 31 url: options.url, 32 }), 33 }); 34 } catch { 35 // Never let notification failures crash your app 36 } 37}

The try/catch is important — you never want a notification failure to bubble up and break user-facing flows.


Using It in a Server Action

1// app/actions/auth.ts 2'use server'; 3 4import { notify } from '@/lib/notify'; 5import { db } from '@/lib/db'; 6import { hash } from 'bcryptjs'; 7 8export async function registerUser(formData: FormData) { 9 const email = formData.get('email') as string; 10 const name = formData.get('name') as string; 11 12 const user = await db.user.create({ 13 data: { email, name, passwordHash: await hash(formData.get('password') as string, 12) }, 14 }); 15 16 // Fire notification in parallel — don't await it in the critical path 17 notify({ 18 event: 'user.registered', 19 title: '👤 New Sign-up', 20 body: `${name} (${email}) just created an account`, 21 priority: 'NORMAL', 22 data: { userId: user.id, email }, 23 }); 24 25 return { success: true }; 26}

By not awaiting the notify() call, it fires in the background without adding any latency to the user's registration experience.


Using It in a Route Handler

1// app/api/contact/route.ts 2import { NextRequest, NextResponse } from 'next/server'; 3import { notify } from '@/lib/notify'; 4 5export async function POST(req: NextRequest) { 6 const body = await req.json(); 7 const { name, email, message } = body; 8 9 // Save to your DB here... 10 11 // Notify in background 12 notify({ 13 event: 'contact.form_submitted', 14 title: '📩 New Contact Form Submission', 15 body: `${name} (${email}): ${message.slice(0, 80)}`, 16 priority: 'HIGH', 17 data: { name, email, message }, 18 }); 19 20 return NextResponse.json({ ok: true }); 21}

Stripe Webhook → Push Notification

This is one of the most valuable use cases. Know the moment money hits your account:

1// app/api/webhooks/stripe/route.ts 2import { NextRequest, NextResponse } from 'next/server'; 3import Stripe from 'stripe'; 4import { notify } from '@/lib/notify'; 5 6const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); 7 8export async function POST(req: NextRequest) { 9 const sig = req.headers.get('stripe-signature')!; 10 const body = await req.text(); 11 12 let event: Stripe.Event; 13 try { 14 event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!); 15 } catch { 16 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 }); 17 } 18 19 if (event.type === 'payment_intent.succeeded') { 20 const pi = event.data.object as Stripe.PaymentIntent; 21 const amount = (pi.amount / 100).toFixed(2); 22 const currency = pi.currency.toUpperCase(); 23 24 notify({ 25 event: 'stripe.payment_succeeded', 26 title: `💰 Payment — ${currency} ${amount}`, 27 body: `Payment from ${pi.receipt_email ?? 'unknown'} confirmed`, 28 priority: 'HIGH', 29 data: { paymentIntentId: pi.id, amount: pi.amount, currency: pi.currency }, 30 }); 31 } 32 33 if (event.type === 'customer.subscription.deleted') { 34 const sub = event.data.object as Stripe.Subscription; 35 notify({ 36 event: 'stripe.subscription_cancelled', 37 title: '⚠️ Subscription Cancelled', 38 body: `Customer ${sub.customer} cancelled their subscription`, 39 priority: 'URGENT', 40 }); 41 } 42 43 return NextResponse.json({ received: true }); 44}

Background Job Failure Alert

If you're running cron jobs or background processing with Vercel Cron or a queue:

1// app/api/cron/sync-orders/route.ts 2import { NextResponse } from 'next/server'; 3import { notify } from '@/lib/notify'; 4 5export const dynamic = 'force-dynamic'; 6 7export async function GET() { 8 try { 9 await syncOrdersFromExternalAPI(); 10 return NextResponse.json({ ok: true }); 11 } catch (error) { 12 // This wakes you up immediately instead of waiting for a monitoring alert 13 await notify({ 14 event: 'cron.sync_orders_failed', 15 title: '🚨 Cron Job Failed: Order Sync', 16 body: error instanceof Error ? error.message : 'Unknown error', 17 priority: 'URGENT', 18 }); 19 return NextResponse.json({ error: 'Sync failed' }, { status: 500 }); 20 } 21}

Note: Here we await the notify call because we want to ensure the alert is sent before returning the error response. In a cron context this is fine.


Routing Rules for Your Next.js App

In the FlowAlert dashboard, set up routing rules that match the event keys you used in your code above:

Event KeyWho Gets NotifiedPriorityRequires Ack
user.registeredFounderNORMALNo
stripe.payment_succeededFinanceHIGHNo
stripe.subscription_cancelledFounder + CSURGENTYes
cron.sync_orders_failedEngineeringURGENTYes

These event keys are examples you define yourself. The key you send in your code and the key on your FlowAlert routing rule just need to match — you can name them whatever makes sense for your project.

You can use cron.* as a single routing rule to catch all cron job failures, or create one rule per event key for more granular routing. A consistent naming convention (e.g. cron.sync_orders_failed, cron.send_emails_failed) makes both approaches easy to manage.


Summary

Sending push notifications from Next.js is just an async fetch call. No service workers, no Firebase project setup, no VAPID keys. Drop the notify() utility into any Server Action or Route Handler and start getting alerted on the events that matter.

Start with FlowAlert →

Get alerts where you’ll actually see them.

Everything in these guides works on the free plan — connect a source, add a routing rule, and the right person’s phone knows in under two minutes.

No card required · Free plan forever · 1,000 events/month