π How to Convert a Next.js App to a Progressive Web App (PWA) β App Router + TypeScript


Next.js has evolved. If you're using the App Router (the new app/
directory-based routing system), you can now build a Progressive Web App (PWA) using official support from the framework. This guide walks you through making your app installable, offline-capable, and even capable of sending web push notifications.
β Why Convert to a PWA?
With a PWA, you can:
Deploy updates without app store approvals
Send push notifications
Allow users to install your app on their device
Provide offline access
Use a single codebase for both web and mobile-like experiences
π Related Reads to Level Up Your Next.js PWA
π Authorization Bypass in Next.js Middleware: How to Secure Your Application
Once your PWA is live, securing your middleware routes becomes critical. This article explains common authorization flaws in Next.js Middleware and gives you practical fixes using modern patterns. Pair this with your service worker and server actions to avoid open access to sensitive push endpoints.
π§ Step-by-Step Guide
1. Create a Web App Manifest
Use app/manifest.ts
to dynamically generate your manifest file in TypeScript:
// app/manifest.ts
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',
},
],
}
}
Place your icon files in public/
.
2. Add Push Notifications
This is optional but powerful.
app/page.tsx
'use client'
import { useState, useEffect } from 'react'
import { subscribeUser, unsubscribeUser, sendNotification } from './actions'
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)))
}
function PushNotificationManager() {
const [subscription, setSubscription] = useState<PushSubscription | null>(null)
useEffect(() => {
if ('serviceWorker' in navigator && 'PushManager' in window) {
navigator.serviceWorker.register('/sw.js')
.then(reg => reg.pushManager.getSubscription())
.then(setSubscription)
}
}, [])
async function subscribeToPush() {
const reg = await navigator.serviceWorker.ready
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!)
})
setSubscription(sub)
await subscribeUser(JSON.parse(JSON.stringify(sub)))
}
async function unsubscribeFromPush() {
await subscription?.unsubscribe()
setSubscription(null)
await unsubscribeUser()
}
return (
<div>
{subscription ? (
<>
<p>Subscribed</p>
<button onClick={unsubscribeFromPush}>Unsubscribe</button>
</>
) : (
<button onClick={subscribeToPush}>Subscribe</button>
)}
</div>
)
}
3. Implement Server Actions
npm install web-push
app/actions.ts
'use server'
import webpush from 'web-push'
webpush.setVapidDetails(
'mailto:your@email.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
return { success: true }
}
export async function unsubscribeUser() {
subscription = null
return { success: true }
}
export async function sendNotification(message: string) {
if (!subscription) throw new Error('No subscription')
await webpush.sendNotification(subscription, JSON.stringify({ title: 'Hello!', body: message }))
return { success: true }
}
4. Generate VAPID Keys
Install the CLI and run:
npm install -g web-push
web-push generate-vapid-keys
Add to .env
:
NEXT_PUBLIC_VAPID_PUBLIC_KEY=...
VAPID_PRIVATE_KEY=...
5. Add a Service Worker
public/sw.js
self.addEventListener('push', function (event) {
const data = event.data.json()
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon.png',
badge: '/badge.png',
})
)
})
self.addEventListener('notificationclick', function (event) {
event.notification.close()
event.waitUntil(clients.openWindow('/'))
})
6. Test Locally with HTTPS
To test push notifications:
next dev --experimental-https
Make sure:
You allow notifications in the browser
You register the service worker before subscribing
π Build a Modern TODO App with Next.js, TypeScript, Tailwind CSS & Mongoose (Full CRUD Functionality!)π
Want to build something useful as a PWA? This full-stack CRUD TODO app tutorial is a perfect example. Once you've built it, wrap it in PWA features β and youβve got a mobile-friendly, installable productivity tool.
7. Add Security Headers
In next.config.js
:
module.exports = {
async headers() {
return [
{
source: '/sw.js',
headers: [
{ key: 'Content-Type', value: 'application/javascript' },
{ key: 'Cache-Control', value: 'no-cache' },
{ key: 'Content-Security-Policy', value: "default-src 'self'" }
]
}
]
}
}
β Done! Your App is Now a PWA
Youβve now:
Created a manifest
Registered a service worker
Enabled push notifications
Secured your service worker
Made your app installable
This setup uses the Next.js App Router and TypeScript, with full control over metadata, icons, service worker behavior, and push.
π¨ Must-Try: Top 10 Cutting-Edge UI Libraries to Boost Your Web Design in 2025
PWAs arenβt just about functionality β design matters too. This article helps you explore modern UI frameworks that make your PWA look and feel like a real native app, from animations to responsive components.
π‘ Pro Tips
Use Lighthouse to audit your PWA
Store subscriptions in a real DB if going to production
Add background sync or offline caching with tools like Serwist (if needed)
Subscribe to my newsletter
Read articles from ByteScrum Technologies directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

ByteScrum Technologies
ByteScrum Technologies
Our company comprises seasoned professionals, each an expert in their field. Customer satisfaction is our top priority, exceeding clients' needs. We ensure competitive pricing and quality in web and mobile development without compromise.