Complete Guide: Push Notifications in Next.js
Learn how to add push notifications to your Next.js application that work even when the browser is closed. This guide will walk you through every step with clear explanations.
What You'll Build
By the end of this guide, you'll have:
- π Push notifications that work when your app is closed
- π± A non-intrusive notification banner
- πΎ Persistent user preferences with localStorage
- π Secure VAPID authentication
- π A database to store user subscriptions
Prerequisites
Before starting, make sure you have:
- Basic knowledge of React and Next.js
- A Next.js 13+ project with App Router
- Node.js and npm installed
- A database setup (we'll use Prisma with PostgreSQL)
How Push Notifications Work
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β Browser βββββββΆβ Your Server βββββββΆβ Browser β
β (User A) β β β β (User B) β
β β β Stores β β β
β Subscribes β β subscriptionβ β Receives β
β to push β β & sends β β notificationβ
β β β notificationsβ β β
βββββββββββββββ ββββββββββββββββ ββββββββββββββββ
Key Components:
- Service Worker - Runs in the background, receives notifications
- Push API - Allows your server to send notifications
- VAPID Keys - Authenticates your server with push services
- Subscription - User's unique identifier for receiving notifications
Step 1: Generate VAPID Keys
VAPID (Voluntary Application Server Identification) keys authenticate your server when sending push notifications.
Install web-push globally
npm install -g web-push
web-push generate-vapid-keys
Generate your keys
You'll see output like this:
=======================================
Public Key:
BKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Private Key:
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
=======================================
Add to your environment variables
Create or update .env.local:
NEXT_PUBLIC_VAPID_PUBLIC_KEY=BKxxxxxxxxxxx...
VAPID_PRIVATE_KEY=yyyyyyyyyyyy...
VAPID_EMAIL=mailto:your-email@example.com
β οΈ Important:
- The public key MUST start with
NEXT_PUBLIC_to be accessible in the browser- Never commit your
.env.localfile to version control- Keep your private key secret!
Step 2: Setup Database Schema
We need to store user subscriptions so we can send them notifications later.
Update your Prisma schema
Add this model to your prisma/schema.prisma:
model PushSubscription {
id String @id @default(uuid())
userId String // Connect to your User model if you have authentication
endpoint String @unique
p256dh String // Encryption key
auth String // Authentication secret
userAgent String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
Run the migration
npx prisma migrate dev --name add_push_subscriptions
Step 3: Create the Service Worker
The service worker runs in the background and handles incoming notifications.
Create public/sw.js
π‘ Tip: The service worker must be in the
public/folder and will be accessible at/sw.js
Step 4: Create the Notification Banner Component
This component handles requesting permission and subscribing users.
Create components/NotificationBanner.tsx
Add to your layout
// app/layout.tsx
import NotificationBanner from '@/components/NotificationBanner';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<NotificationBanner />
</body>
</html>
);
}
Step 5: Create API Routes
5.1 Subscribe Route
This saves user subscriptions to your database.
Create app/api/subscribe/route.ts:
5.2 Send Notification Route
This sends push notifications to subscribed users.
Create app/api/send-notification/route.ts:
Step 6: Testing Your Implementation
Test Locally with HTTPS
Push notifications require HTTPS. Use Next.js experimental HTTPS:
npm install -D @next/experimental-https
Then run:
next dev --experimental-https
Your app will be available at https://localhost:3000
Test the Flow
- Open your app at
https://localhost:3000 - Wait 3 seconds for the banner to appear
- Click "Enable" and accept the browser permission
- Check your terminal to see the subscription saved
- Close the browser completely
- Send a test notification (see below)
Send a Test Notification
Use this API route or create a test page:
// Test from your terminal or API client
curl -X POST https://localhost:3000/api/send-notification \
-H "Content-Type: application/json" \
-d '{
"title": "Test Notification π",
"body": "This is a test push notification!",
"url": "/dashboard"
}'
Or create a test button in your app:
// components/TestNotificationButton.tsx
'use client';
export default function TestNotificationButton() {
const sendTest = async () => {
const res = await fetch('/api/send-notification', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'π Test Notification',
body: 'If you see this, push notifications are working!',
url: '/',
}),
});
const data = await res.json();
alert(`Sent ${data.sent} notifications!`);
};
return (
<button
onClick={sendTest}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Send Test Notification
</button>
);
}
Step 7: Sending Notifications from Your Backend
Example: Send notification when a new event occurs
// Example: In your API route or server action
import { PrismaClient } from '@prisma/client';
import webpush from 'web-push';
const prisma = new PrismaClient();
webpush.setVapidDetails(
process.env.VAPID_EMAIL!,
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
);
async function notifyUser(userId: string, message: string) {
const subscriptions = await prisma.pushSubscription.findMany({
where: { userId },
});
const payload = JSON.stringify({
title: 'π Security Alert',
body: message,
icon: '/icon-192x192.png',
data: { url: '/security' },
});
await Promise.all(
subscriptions.map((sub) =>
webpush.sendNotification(
{
endpoint: sub.endpoint,
keys: { p256dh: sub.p256dh, auth: sub.auth },
},
payload
)
)
);
}
// Usage
await notifyUser('user123', 'Your password will expire in 3 days');
Troubleshooting
Issue: "Permission denied"
Solution: The user clicked "Block" on the notification permission. They need to:
- Click the lock icon in the address bar
- Find "Notifications" and change to "Allow"
- Refresh the page and try again
Issue: "Service Worker not supported"
Solution:
- Make sure you're using a modern browser (Chrome 42+, Firefox 44+, Edge 17+)
- Safari on iOS requires iOS 16.4+ and the app must be installed to home screen
Issue: Notifications not showing when app is closed
Solution:
- Ensure you're using HTTPS (required for service workers)
- Check that the service worker is registered: Go to DevTools β Application β Service Workers
- Verify subscription is saved in your database
- Test with
curlto rule out frontend issues
Issue: "Application server key not found"
Solution: Your NEXT_PUBLIC_VAPID_PUBLIC_KEY environment variable is not set correctly. Make sure:
- It starts with
NEXT_PUBLIC_ - You've restarted your dev server after adding it
- The key is correct (starts with
Band is base64 encoded)
Best Practices
1. Don't Spam Users
- Only send relevant, valuable notifications
- Allow users to customize notification preferences
- Respect "Do Not Disturb" hours
2. Handle Unsubscribes
Add an unsubscribe option:
// app/api/unsubscribe/route.ts
export async function POST(req: NextRequest) {
const { endpoint } = await req.json();
await prisma.pushSubscription.delete({
where: { endpoint },
});
return NextResponse.json({ success: true });
}
3. Clean Up Invalid Subscriptions
The send notification route already handles this - it removes subscriptions that return 410 (Gone) errors.
4. Add User Preferences
Let users choose what notifications they want:
model NotificationPreferences {
id String @id @default(uuid())
userId String @unique
securityAlerts Boolean @default(true)
marketingUpdates Boolean @default(false)
productUpdates Boolean @default(true)
}
5. Test Across Browsers
Different browsers handle notifications differently:
- Chrome/Edge: Full support, works when closed
- Firefox: Full support, works when closed
- Safari (macOS): Requires macOS 13+, works when closed
- Safari (iOS): Requires iOS 16.4+, must be installed to home screen
Production Checklist
Before deploying to production:
- [x] VAPID keys are set in production environment variables
- [x] Service worker is accessible at
/sw.js - [x] Database schema is migrated
- [x] HTTPS is enabled
- [x] Error handling is implemented
- [x] Invalid subscriptions are cleaned up automatically
- [x] User can unsubscribe
- [x] Notification content is relevant and valuable
- [x] Icons and badges are properly sized and optimized
- [x] Testing completed on multiple browsers
- [x] Privacy policy mentions push notifications
Next Steps
Now that you have push notifications working, consider:
- Add notification preferences - Let users choose what they want to be notified about
- Implement notification history - Show past notifications in your app
- Add rich actions - Allow users to take actions directly from notifications
- Schedule notifications - Use cron jobs or queue systems to send at specific times
- Analytics - Track notification open rates and engagement
Additional Resources
Summary
You've successfully implemented push notifications that:
β
Work even when the browser is closed
β
Use a non-intrusive banner for opt-in
β
Store user preferences in localStorage
β
Save subscriptions to a database
β
Can be sent from your backend to specific users or all users
β
Handle errors and clean up invalid subscriptions
Your users can now stay engaged with your app even when they're not actively using it! π
βοΈ Written by Moses Kisakye
- Published on October 31, 2025
