
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.
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 needed2. Add your API key to environment variables
# .env.local
FLOWALERT_API_KEY=fa_live_xxxxxxxxxxxxNever 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 Key | Who Gets Notified | Priority | Requires Ack |
|---|---|---|---|
user.registered | Founder | NORMAL | No |
stripe.payment_succeeded | Finance | HIGH | No |
stripe.subscription_cancelled | Founder + CS | URGENT | Yes |
cron.sync_orders_failed | Engineering | URGENT | Yes |
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.


