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

dev.familydev.family
7 min read

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 need

  • offline.html – fallback for when the user’s offline

  • manifest.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 route

  • sseEndpoint – 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 page

  • REVALIDATE_URL – force refresh a specific URL

  • DISABLE_CACHE / ENABLE_CACHE – toggle caching

  • SKIP_WAITING – activate new SW version

  • CLEAR_STATIC_CACHE – drop static/API cache (useful after SSE updates)

Offline Mode

  • Shows offline.html when offline and no cache available

  • Tries 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!

0
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.