I like to build tools that don’t just work — they get out of your way. My team has been in the trenches of Next.js long enough to know exactly what hurts. One of the latest things I put together is next-pwa-pack — a drop-in package that wires up full PWA support in your Next.js app without you tearing your hair out.

The Backstory (aka: Why I Wrote This Thing)

Every time a client mentioned “PWA support,” I braced myself.

I tried existing libraries. Too much magic. Tons of config. Or just completely incompatible with App Router — which, by the way, we’ve fully adopted. I wanted:

Instead, I ended up writing service workers by hand. Tuning cache TTLs. Dealing with update logic. Managing stale clients. Manually wiping caches every time we shipped.

And don’t even get me started on users not seeing updates until they hard-refreshed. Enough was enough.

I needed something dead simple, predictable, and battle-tested. So I built it.

Building next-pwa-pack: What Went Into It

Step one was writing a minimal service worker:

Then I added a messaging system so the client could talk to the service worker — for example, to bust a cache or disable caching entirely.

Next, I wrote a few scripts:

For full App Router and SSR/Edge support, I wrapped everything in a higher-order function: withPWA. One import, one call — done.

I also built in tab synchronization. Because users will open your app in 3 tabs and expect them to magically update in sync. I solved that via localStorage + storage events.

The result? A package that just works. No config black magic. No rewriting core parts of your app.

What You Get with next-pwa-pack

Once installed, you get:

You can grab the package here: 👉 https://github.com/dev-family/next-pwa-pack

What Happens When You Install It

The install script auto-copies PWA boilerplate into /public:

⚠️ Existing files won’t be overwritten — it respects your setup.

If you want to trigger the copy manually:

node node_modules/next-pwa-pack/scripts/copy-pwa-files.mjs
# or
npx next-pwa-pack/scripts/copy-pwa-files.mjs

The server action revalidatePWA is also added to your app/actions.ts or src/app/actions.ts file depending on your folder structure:

"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();
}

If that file doesn’t show up, you can run:

node node_modules/next-pwa-pack/scripts/copy-pwa-server-actions.mjs

Configuring Your manifest.json

After install, don’t forget to customize /public/manifest.json:

{
 "name": "My App",
 "short_name": "App",
 "description": "My amazing PWA 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"
   }
 ]
}

Drop your icons into public/icons/, or tweak the paths above. Nothing fancy.

Quick Start: Wire It Up

Wrap your layout in the PWAProvider, and the magic kicks in:

import { PWAProvider } from "next-pwa-pack";

export default function layout({ children }) {
  return <PWAProvider>{children}</PWAProvider>;
}

If you want revalidation to work from the server side, you’ll also need to update your middleware:

// /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*"],
};

The HOC Options:

Inside the PWAProvider

The PWAProvider bundles a bunch of stuff under the hood — and you can cherry-pick components too:

RegisterSW

Automatically registers the service worker (/sw.js). Handles errors gracefully. You can override the path if needed:

<PWAProvider swPath="/custom/sw.js">{children}</PWAProvider>

CacheCurrentPage

Intercepts navigation (including SPA-style transitions), caches the current page’s HTML.

SWRevalidateListener

Watches for localStorage events and triggers cache refresh across tabs.

SSERevalidateListener

Listens to server-sent events from the sseEndpoint. When your backend says “revalidate these URLs,” this listener makes sure clients do it.

DevPWAStatus

Dev-only panel you can enable like this:

<PWAProvider devMode>{children}</PWAProvider>

Shows:

What the Service Worker Actually Does

The core sw.js handles:

HTML Caching

Static Assets

Messaging Support

Supports these actions from the client:

Offline Mode

Using withPWA in Middleware

This is where next-pwa-pack really earns its keep. It brings cache revalidation to SSR and Edge Middleware — with SSE support and all.

export default withPWA(originalMiddleware, {
  revalidationSecret: process.env.REVALIDATION_SECRET!,
  sseEndpoint: "/api/pwa/cache-events",
  webhookPath: "/api/pwa/revalidate",
});

Params:

Real-World Use Cases

Updating Cache After Data Changes

import { updateSWCache } from "next-pwa-pack";

// After creating a blog post:
const handleCreatePost = async (data) => {
  await createPost(data);
  updateSWCache(["/blog", "/dashboard"]);
};

Updating Cache From the Server

import { revalidatePWA } from "../actions";

await createPost(data);
await revalidatePWA(["/my-page"]);

Clearing Cache on Logout

import { clearAllCache } from "next-pwa-pack";

const handleLogout = async () => {
  await logout();
  await clearAllCache();
  router.push("/login");
};

All Client Cache Actions

import {
  clearAllCache,
  reloadServiceWorker,
  updatePageCache,
  unregisterServiceWorkerAndClearCache,
  updateSWCache,
  disablePWACache,
  enablePWACache,
  clearStaticCache,
  usePWAStatus,
} from "next-pwa-pack";

// Examples:
await clearAllCache();
await reloadServiceWorker();
await updatePageCache("/about");
await unregisterServiceWorkerAndClearCache();
await clearStaticCache();
updateSWCache(["/page1", "/page2"]);
disablePWACache();
enablePWACache();

const { online, hasUpdate, swInstalled, update } = usePWAStatus();

API Route for External Cache Triggers

If you want to trigger cache refreshes externally (e.g., from an admin panel), here’s an API route you can use:

// app/api/webhook/revalidate/route.ts

import { NextRequest, NextResponse } from "next/server";
import { revalidatePWA } from "@/app/actions";
import { revalidateTag } from "next/cache";
import { FetchTags } from "@/app/api/endpoints/backend";

export async function POST(request: NextRequest) {
  try {
    const { tags, secret, urls } = await request.json();

    if (secret !== process.env.REVALIDATION_SECRET) {
      return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
    }

    const validTags = Object.values(FetchTags);
    const invalidTags = tags?.filter((tag) => !validTags.includes(tag)) || [];

    if (invalidTags.length > 0) {
      return NextResponse.json(
        { error: `Invalid tags: ${invalidTags.join(", ")}` },
        { status: 400 }
      );
    }

    let successful = 0;
    let failed = 0;

    if (tags?.length) {
      const tagResults = await Promise.allSettled(
        tags.map((tag) => revalidateTag(tag))
      );
      successful = tagResults.filter((r) => r.status === "fulfilled").length;
      failed = tagResults.filter((r) => r.status === "rejected").length;
    }

    if (urls?.length) {
      await revalidatePWA(urls);
    }

    return NextResponse.json({
      success: true,
      message: "Cache revalidation completed",
      tags,
      urls,
      successful,
      failed,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    console.error("Webhook revalidation error:", error);
    return NextResponse.json({ error: "Internal server error" }, { status: 500 });
  }
}

Hit it with:

POST https://your-app.com/api/webhook/revalidate
{
  "tags": ["faq"],
  "secret": "1234567890",
  "urls": ["/ru/question-answer"]
}

Debugging & DevTools

Here’s what you can check when debugging:

Gotchas & Notes

A few things you should know before you ship:

Security

Performance

Config

export default function PWAProvider({
  children,
  swPath,
  devMode = false,
  serverRevalidation = {
    enabled: true,
    sseEndpoint: "/api/pwa/cache-events"
  },
}: PWAProviderProps) {

What’s Next

next-pwa-pack is written for Next.js 15. It should work on Next.js 13 App Router as well — just not tested extensively.

Planned features:

That’s it.

If you’re tired of wrangling service workers manually, give next-pwa-pack a shot. You’ll go from zero to full PWA support in one coffee break.

Questions, bugs, or feedback? Open an issue or hit us up.

👉 github.com/dev-family/next-pwa-pack