Documentation Index
Fetch the complete documentation index at: https://mintlify.com/vercel/next.js/llms.txt
Use this file to discover all available pages before exploring further.
Progressive Web Applications (PWAs) combine the reach of web apps with native app features like offline support, push notifications, and home screen installation—all from a single codebase without app store approvals.
Creating a PWA
Create the web app manifest
Create app/manifest.ts to define your app’s metadata:import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Next.js PWA',
short_name: 'NextPWA',
description: 'A Progressive Web App built with Next.js',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
icons: [
{
src: '/icon-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/icon-512x512.png',
sizes: '512x512',
type: 'image/png',
},
],
}
}
Use a favicon generator to create icon sets and place them in public/. Implement push notifications
Create the main page component with push notification management:'use client'
import { useState, useEffect } from 'react'
import { subscribeUser, unsubscribeUser, sendNotification } from './actions'
function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}
function PushNotificationManager() {
const [isSupported, setIsSupported] = useState(false)
const [subscription, setSubscription] = useState<PushSubscription | null>(null)
const [message, setMessage] = useState('')
useEffect(() => {
if ('serviceWorker' in navigator && 'PushManager' in window) {
setIsSupported(true)
registerServiceWorker()
}
}, [])
async function registerServiceWorker() {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
updateViaCache: 'none',
})
const sub = await registration.pushManager.getSubscription()
setSubscription(sub)
}
async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready
const sub = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
),
})
setSubscription(sub)
const serializedSub = JSON.parse(JSON.stringify(sub))
await subscribeUser(serializedSub)
}
async function unsubscribeFromPush() {
await subscription?.unsubscribe()
setSubscription(null)
await unsubscribeUser()
}
if (!isSupported) {
return <p>Push notifications are not supported in this browser.</p>
}
return (
<div>
<h3>Push Notifications</h3>
{subscription ? (
<>
<p>You are subscribed to push notifications.</p>
<button onClick={unsubscribeFromPush}>Unsubscribe</button>
<input
type="text"
placeholder="Enter notification message"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button onClick={() => sendNotification(message)}>Send Test</button>
</>
) : (
<>
<p>You are not subscribed to push notifications.</p>
<button onClick={subscribeToPush}>Subscribe</button>
</>
)}
</div>
)
}
function InstallPrompt() {
const [isIOS, setIsIOS] = useState(false)
const [isStandalone, setIsStandalone] = useState(false)
useEffect(() => {
setIsIOS(/iPad|iPhone|iPod/.test(navigator.userAgent))
setIsStandalone(window.matchMedia('(display-mode: standalone)').matches)
}, [])
if (isStandalone) return null
return (
<div>
<h3>Install App</h3>
{isIOS && (
<p>Tap the share button and then "Add to Home Screen" to install this app.</p>
)}
</div>
)
}
export default function Page() {
return (
<div>
<PushNotificationManager />
<InstallPrompt />
</div>
)
}
Create Server Actions for notifications
'use server'
import webpush from 'web-push'
webpush.setVapidDetails(
'mailto:your-email@example.com',
process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
)
let subscription: PushSubscription | null = null
export async function subscribeUser(sub: PushSubscription) {
subscription = sub
// In production: store in database
return { success: true }
}
export async function unsubscribeUser() {
subscription = null
// In production: remove from database
return { success: true }
}
export async function sendNotification(message: string) {
if (!subscription) throw new Error('No subscription available')
try {
await webpush.sendNotification(
subscription,
JSON.stringify({
title: 'Test Notification',
body: message,
icon: '/icon.png',
})
)
return { success: true }
} catch (error) {
console.error('Error sending push notification:', error)
return { success: false, error: 'Failed to send notification' }
}
}
Generate VAPID keys
Install web-push globally and generate keys:npm install -g web-push
web-push generate-vapid-keys
Add the output to your .env file:NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key_here
VAPID_PRIVATE_KEY=your_private_key_here
Create the service worker
self.addEventListener('push', function (event) {
if (event.data) {
const data = event.data.json()
const options = {
body: data.body,
icon: data.icon || '/icon.png',
badge: '/badge.png',
vibrate: [100, 50, 100],
data: {
dateOfArrival: Date.now(),
primaryKey: '2',
},
}
event.waitUntil(self.registration.showNotification(data.title, options))
}
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(clients.openWindow('https://your-website.com'))
})
Add security headers
Configure security headers in next.config.js, especially for the service worker:module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
{
source: '/sw.js',
headers: [
{ key: 'Content-Type', value: 'application/javascript; charset=utf-8' },
{ key: 'Cache-Control', value: 'no-cache, no-store, must-revalidate' },
{ key: 'Content-Security-Policy', value: "default-src 'self'; script-src 'self'" },
],
},
]
},
}
Home screen installation
For a PWA to be installable, you need:
- A valid web app manifest (created in step 1)
- The site served over HTTPS
Modern browsers automatically show an install prompt when these criteria are met. For iOS, users must manually tap Share → Add to Home Screen.
Testing locally
To test push notifications locally:
next dev --experimental-https
- Accept notification permissions when prompted
- Ensure browser notifications are not globally disabled
Offline support
For offline functionality, use Serwist with Next.js. See their documentation for integration steps.
The Serwist plugin currently requires webpack configuration.