Simple PWA Integration in Next.js with next-pwa-pack


We usually build things that we'd actually want to use myself. And we love to share them with everyone who might need it! One of those tools is the shiny new package: next-pwa-pack
. It lets you bolt on solid PWA support into your Next.js app in no time.
Why We Built This Thing
Sometimes clients ask us to "just add PWA support", which is usually code for “lose two days of your life fiddling with service workers and wondering why stuff randomly breaks.” We tried existing packages. Most were bloated, over-engineered, or just didn’t play nice with the App Router in Next.js.
Also, there was always something missing, like support for server actions to control cache, integration with SSR, or even just the ability to plug it cleanly into App Router without needing a spirit guide.
So we’d end up hand-rolling service workers, configuring cache logic from scratch, debugging offline mode across tabs… and every code update meant manually nuking the cache. Plus, no update notifications for users. Fun.
What we needed was a dead-simple, batteries-included PWA solution. No deep dives into service worker specs.
Building the Package
Step one: we listed every annoying thing we had to do each time someone asked for a PWA. The mission: make it so a developer could get from zero to working PWA with minimal ceremony.
Started with a basic service worker that:
Caches HTML pages with TTL
Caches static assets
Handles offline mode
Then we added a messaging system between the client and the service worker so we could control the cache programmatically. Wrote a couple scripts to auto-copy the required files (sw.js
, manifest.json
, offline.html
) into your project on install.
Also auto-injected a server action called revalidatePWA
so you can revalidate cached pages from the server — via server actions, API routes, or server components.
For SSR/Edge Middleware and App Router integration, we built a HOC called withPWA
. Now you can plug in server-driven revalidation and cache updates even in gnarly routing setups.
Bonus headache: syncing cache across tabs in SPA-mode. That’s in too — via localStorage
+ storage
events.
In the end, we got a package that just works out of the box (no black magic!).
Why Use next-pwa-pack
?
Installing this package gives you:
Auto service worker registration — no need to DIY that boilerplate
Project-specific files auto-copied — tweak them however you want
Cache control utilities — update, nuke, or disable cache easily
Tab sync — keeps caches aligned across browser tabs
Offline mode — yes, your app works without internet
Dev tools — built-in debug panel for dev-mode
Server-side revalidation — works with server actions, API routes, and external systems
Grab it here: https://github.com/dev-family/next-pwa-pack
What Happens on Install
The following files are auto-copied into your public
folder:
sw.js
– service worker with all the logic you needoffline.html
– fallback for when the user’s offlinemanifest.json
– your PWA config file
⚠️ Already got those filenames? We won’t overwrite them. You can copy manually using:
node node_modules/next-pwa-pack/scripts/copy-pwa-files.mjs
# or
npx next-pwa-pack/scripts/copy-pwa-files.mjs
Also, we auto-create (or update) a server action:
// app/actions.ts OR src/app/actions.ts
"use server";
export async function revalidatePWA(urls: string[]) {
const baseUrl = process.env.NEXT_PUBLIC_HOST || "http://localhost:3000";
const res = await fetch(`${baseUrl}/api/pwa/revalidate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
urls,
secret: process.env.REVALIDATION_SECRET,
}),
});
return res.json();
}
Didn’t get the file? Run:
node node_modules/next-pwa-pack/scripts/copy-pwa-server-actions.mjs
Configuring manifest.json
After install, tweak public/manifest.json
for your project:
{
"name": "My App",
"short_name": "My App",
"description": "Description of my app",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Put your icons into public/icons/
or update paths in the manifest.
Quick Start
Wrap your app in the PWAProvider
. That’s it. It wires up the rest.
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider>{children}</PWAProvider>;
}
For cache revalidation on the server, wrap your middleware in the HOC:
// /middleware.ts
import { withPWA } from "next-pwa-pack/hoc/withPWA";
function originalMiddleware(request) {
// your logic here
return response;
}
export default withPWA(originalMiddleware, {
revalidationSecret: process.env.REVALIDATION_SECRET!,
sseEndpoint: "/api/pwa/cache-events",
webhookPath: "/api/pwa/revalidate",
});
export const config = {
matcher: ["/", "/(ru|en)/:path*", "/api/pwa/:path*"],
};
HOC Arguments:
originalMiddleware
– your middleware (e.g., auth, i18n)revalidationSecret
– keeps strangers out of your revalidation routesseEndpoint
– server-sent events endpoint (change it if needed)webhookPath
– POST endpoint to trigger cache revalidation manually
Now you can call revalidatePWA
from anywhere — server actions, server components, API routes. The rest is handled for you.
Need a PWA app built? Hit us up.
What’s Inside PWAProvider
This is where the magic happens. Here’s what it wires up:
RegisterSW
Auto-registers your service worker. Checks support, registers /sw.js
, logs errors if something explodes.
CacheCurrentPage
Intercepts navigation (SPA included) and sends the HTML to the service worker for caching. Supports offline mode and faster reloads.
SWRevalidateListener
Listens for localStorage
events and syncs cache across tabs. When one tab updates, others follow.
SSERevalidateListener
Listens for server-sent events at /api/pwa/cache-events
. When the server says “revalidate,” the client updates the cache. Crucial for SSR + server action integration.
DevPWAStatus
Built-in dev panel. Enable with devMode
:
<PWAProvider devMode>{children}</PWAProvider>
Gives you:
Online/offline status
Update notifications
Buttons to:
Clear cache
Reload SW
Update page cache
Unregister SW
Toggle caching
What the Service Worker Does
TL;DR: It’s a background script that manages cache, network, and updates.
HTML Caching
TTL defaults to 10 minutes (change in
/sw.js
)Auto-refreshes cache when TTL expires
Offline? Serves cached version
Want to use a custom SW path?
<PWAProvider swPath="/some-path/sw.js">{children}</PWAProvider>
Static Asset Caching
Caches CSS, JS, images forever
Improves load speed on repeat visits
Only works with GET requests (because security)
Message Handling
SW listens for 6 types of messages:
CACHE_CURRENT_HTML
– cache current pageREVALIDATE_URL
– force refresh a specific URLDISABLE_CACHE
/ENABLE_CACHE
– toggle cachingSKIP_WAITING
– activate new SW versionCLEAR_STATIC_CACHE
– drop static/API cache (useful after SSE updates)
Offline Mode
Shows
offline.html
when offline and no cache availableTries to update content once you're back online
The withPWA
HOC
Adds server-side revalidation support via SSR or middleware. Server can broadcast cache updates via SSE, which the client listens for and responds to.
export default withPWA(originalMiddleware, {
revalidationSecret: process.env.REVALIDATION_SECRET!,
sseEndpoint: "/api/pwa/cache-events",
webhookPath: "/api/pwa/revalidate",
});
Usage Examples
Update Cache After Posting Data
import { updateSWCache } from "next-pwa-pack";
const handleCreatePost = async (data) => {
await createPost(data);
updateSWCache(["/blog", "/dashboard"]);
};
Revalidate on Server
import { revalidatePWA } from "../actions";
await createPost(data);
await revalidatePWA(["/my-page"]);
Clear Cache on Logout
import { clearAllCache } from "next-pwa-pack";
const handleLogout = async () => {
await logout();
await clearAllCache();
router.push("/login");
};
All Exported Client Actions
import {
clearAllCache,
reloadServiceWorker,
updatePageCache,
unregisterServiceWorkerAndClearCache,
updateSWCache,
disablePWACache,
enablePWACache,
clearStaticCache,
usePWAStatus,
} from "next-pwa-pack";
await clearAllCache();
await reloadServiceWorker();
await updatePageCache("/about");
await unregisterServiceWorkerAndClearCache();
await clearStaticCache();
updateSWCache(["/page1", "/page2"]);
disablePWACache();
enablePWACache();
const { online, hasUpdate, swInstalled, update } = usePWAStatus();
External Revalidation API Route
Sometimes you want to nuke cache from outside — like after a CMS update. Create an API route:
// app/api/webhook/revalidate/route.ts
...
Then hit it like this:
POST https://your-app/api/webhook/revalidate
body:
{
"tags": ["faq"],
"secret": "1234567890",
"urls": ["/ru/question-answer"]
}
You’ll get a nice JSON with success stats.
Debugging Tips
Cache Verification
Open DevTools → Application → Service Workers
Check registration
Look in Cache Storage →
html-cache-v2
Test Offline Mode
Enable
devMode
Kill your internet (DevTools → Network → Offline)
Refresh page — you should see
offline.html
Logs
Look out for these:
[PWA] Service Worker registered
[SW] Cached: /about
[SW] Revalidated and updated cache for: /blog
Limitations and Gotchas
Security
HTTPS only in prod
Only GET requests are cached
Sensitive data = keep it out of cache
Performance
Doesn’t slow things down
Speeds up repeat visits
Config Stuff
TTL is hardcoded in
sw.js
Exclude URLs via
CACHE_EXCLUDE
You’ll need to hand-edit
manifest.json
PWAProvider
Props
export default function PWAProvider({
children,
swPath,
devMode = false,
serverRevalidation = { enabled: true, sseEndpoint: "/api/pwa/cache-events" },
}: PWAProviderProps) {
Final Word
next-pwa-pack
is a fast, zero-hassle way to turn your Next.js app into a proper PWA. It takes care of all the annoying stuff and gives you clean APIs to control your service worker and cache.
Coming soon:
TTL config via a proper config file
Push notifications
Smarter URL-based cache rules
Cache performance metrics
Built for Next.js 15, but should work fine with App Router in v13+.
Questions? Bugs? Weird use cases? Ping us!
Subscribe to my newsletter
Read articles from dev.family directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

dev.family
dev.family
Hi! We're dev.family or family of developers. It doesn't mean that we're all blood kin, but we are all united by the love of cool projects, complex and interesting tasks and technologies. dev.family is outsourcing company with a huge background and team of 30 wonderful peolple. We are focused on Custom Web & Mobile. Our main focus: e-commerce & food tech. Our capabilities range from product strategy, product design & development, and ongoing product maintenance services.