> ## 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 Apps

> Learn how to build a Progressive Web App (PWA) with Next.js, including the manifest, service workers, push notifications, and home screen installation.

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

<Steps>
  <Step title="Create the web app manifest">
    Create `app/manifest.ts` to define your app's metadata:

    ```ts filename="app/manifest.ts" theme={null}
    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](https://realfavicongenerator.net/) to create icon sets and place them in `public/`.
  </Step>

  <Step title="Implement push notifications">
    Create the main page component with push notification management:

    ```tsx filename="app/page.tsx" theme={null}
    '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>
      )
    }
    ```
  </Step>

  <Step title="Create Server Actions for notifications">
    ```ts filename="app/actions.ts" theme={null}
    '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' }
      }
    }
    ```
  </Step>

  <Step title="Generate VAPID keys">
    Install `web-push` globally and generate keys:

    ```bash theme={null}
    npm install -g web-push
    web-push generate-vapid-keys
    ```

    Add the output to your `.env` file:

    ```bash filename=".env" theme={null}
    NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key_here
    VAPID_PRIVATE_KEY=your_private_key_here
    ```
  </Step>

  <Step title="Create the service worker">
    ```js filename="public/sw.js" theme={null}
    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'))
    })
    ```
  </Step>

  <Step title="Add security headers">
    Configure security headers in `next.config.js`, especially for the service worker:

    ```js filename="next.config.js" theme={null}
    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'" },
            ],
          },
        ]
      },
    }
    ```
  </Step>
</Steps>

## Home screen installation

For a PWA to be installable, you need:

1. A valid web app manifest (created in step 1)
2. 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:

```bash theme={null}
next dev --experimental-https
```

* Accept notification permissions when prompted
* Ensure browser notifications are not globally disabled

## Offline support

For offline functionality, use [Serwist](https://github.com/serwist/serwist) with Next.js. See their [documentation](https://github.com/serwist/serwist/tree/main/examples/next-basic) for integration steps.

<Note>
  The Serwist plugin currently requires webpack configuration.
</Note>
