At dev.family, we often build internal tools that make our workflow smoother — and when something proves useful, we share it with the community. One of those tools is our new package: next-pwa-pack, which is designed to help you quickly add PWA functionality to your Next.js app with minimal setup.
Why I Built This Package
From time to time, clients ask us to add PWA support to their apps, and, honestly, it's often a real headache. I started by trying out existing libraries, but they were either overly complex with tons of boilerplate or simply didn’t work well with the Next.js App Router.
Most solutions lacked support for server actions to control the cache, had poor server-side rendering (SSR) integration, or made it difficult to integrate with the App Router to handle server-side cache revalidation or API-route-based logic.
I found myself writing service workers from scratch, configuring caching manually, dealing with update flows, and figuring out how to sync state across tabs. Just getting a basic PWA setup to work took a lot of time. What annoyed me most was having to clear the cache manually after every code change – and users not getting notified about new versions at all.
Building the Package
First, I developed a basic service worker that handles caching of HTML pages with a configurable TTL (time-to-live), static assets, and offline mode support.
Next, I added a messaging system between the client and the service worker to dynamically manage the cache. Additionally, I wrote a couple of setup scripts that automatically copy the necessary files **(sw.js,** **manifest.json**, **offline.html**) into the project during installation. The scripts also add a built-in `revalidatePWA server action for cache management, which is usable via Server Actions, API routes, or server components.
For integration with SSR/Edge middleware and Next.js App Router, the HOC `withPWA` has been implemented, which allows you to easily connect PWA functionality and server-side cache revalidation even in complex routing and server-side rendering scenarios.
To support server-side rendering (SSR), Edge middleware, and the Next.js App Router, I introduced the HOC `withPWA`. This makes integrating PWA features and server-side cache revalidation easy, even in complex routing and rendering scenarios.
One of the trickier parts was synchronizing the cache across tabs, which is crucial for single-page application (SPA)-style apps, where users might have multiple tabs open. I solved this issue by using localStorage and the storage event to broadcast updates.
The end result is a package that works out of the box – no complicated configuration is required.
Looking for a development partner? Tell us about your project
Why Use next-pwa-pack
Installing next-pwa-pack helps solve several common challenges in PWA development:
- Automated service worker registration – no need to manually write boilerplate code for registering and managing the service worker;
- Essential files included and editable – core assets like sw.js, manifest.json, and offline.html are copied into your project and can be customized to fit your needs;
- Cache management – built-in utilities for clearing, updating, and disabling cache;
- Cross-tab synchronization – ensures the cache stays up-to-date across all open tabs;
- Offline mode support – your app remains usable even when the network is down;
- Developer-friendly tools – includes a built-in dev panel for inspecting and controlling PWA state;
- Server-side cache updates and revalidation – built-in support for server actions, API routes, and integration with external systems.
⚡You can download next-pwa-pack here: https://github.com/dev-family/next-pwa-pack
What Happens During Installation
When you install the package the following essential files are automatically copied into your project’s public folder:
- sw.js – a preconfigured service worker with built-in caching logic;
- offline.html – the offline fallback page;
- manifest.json – your PWA configuration (you’ll need to customize it for your project).
❗These files won’t be copied if files with the same names already exist in your project.
If you want to copy them manually, you can run the following script:
node node_modules/next-pwa-pack/scripts/copy-pwa-files.mjs
# or
npx next-pwa-pack/scripts/copy-pwa-files.mjs
**A server action file is automatically added (or updated)**
`revalidatePWA` – app/actions.ts or src/app/actions.ts (if you're using a src-based project 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 the file doesn’t appear, run:
node node_modules/next-pwa-pack/scripts/copy-pwa-server-actions.mjs
Configuring manifest.json
After installation, update public/manifest.json to match your project’s settings:
{
"name": "My app",
"short_name": "My app",
"description": "My app’s description",
"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"
}
]
}
Add your own icons to the public/icons/folder, or update the paths in manifest.json accordingly.
Quick Start
Next, wrap your application with the PWAProvider component – all the necessary functionality will be enabled automatically:
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider>{children}</PWAProvider>;
}
To enable server-side cache revalidation, also wrap your app with the provided HOC in the /middleware.ts file:
// /middleware.ts
import { withPWA } from "next-pwa-pack/hoc/withPWA";
function originalMiddleware(request) {
// ...your logic
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 existing middleware function (e.g. for i18n, auth, etc.)
- `revalidationSecret` — secret token used to authorize revalidation requests, so only you can trigger them.
- `sseEndpoint` — the endpoint for SSE events (defaults to /api/pwa/cache-events`). Change it if your app is already using this route
- `webhookPath` — the endpoint for webhook-based revalidation (defaults to `/api/pwa/revalidate`). This accepts POST requests to trigger cache revalidation from the server or external systems, and is tied to the revalidatePWA function
**Important:**
In `config.matcher` be sure to specify the paths that should be handled by this middleware (e.g. the site root, localized routes, and PWA-related endpoints).
Now you can call revalidatePWA from server actions, server components, or API Routes to trigger PWA revalidation by URL.
That’s it – the rest is handled by the package!
What’s Inside PWAProvider
PWAProvider brings together several core components that enable full PWA functionality – including automatic caching, cross-tab synchronization, server-side cache revalidation support, and dev tools.
RegisterSW
The RegisterSW component handles service worker registration. It checks if service workers are supported in the browser and registers the service worker file (by default, /sw.js). If registration fails, an error is logged to the console.
CacheCurrentPage
CacheCurrentPage takes care of caching the current page. It intercepts navigation events (including SPA-style route changes) and sends the page’s HTML content to the service worker for caching. This enables offline access and faster page reloads.
SSERevalidateListener
SWRevalidateListener listens for changes in localStorage and updates the cache for specified URLs accordingly. This ensures cache synchronization across all open browser tabs — so if one tab updates the cache, others will follow.
SSERevalidateListener
SSERevalidateListener listens for Server-Sent Events (SSE) from a defined endpoint (default: /api/pwa/cache-events). It allows the server to trigger cache updates on the client – for example, after a mutation or an external webhook call.
This is a key component for enabling server-driven cache revalidation and integrating with Server Actions, API Routes, or external systems. If the server sends an event with the type revalidate and a list of URLs, SSERevalidateListener will automatically update the cache for those pages.
DevPWAStatus
The DevPWAStatus component provides a developer panel (enabled via the devMode prop in PWAProvider).
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider devMode>{children}</PWAProvider>;
}
It displays the current online/offline status, checks for available updates, and lets you perform various actions during development:
- Clear cache;
- Reload the service worker;
- Refresh the current page’s cache;
- Unregister the service worker and remove all cached data;
- Enable or disable PWA caching.
What Does the Service Worker Do?
A service worker is a background script that manages caching, network requests, and app updates. In next-pwa-pack, the service worker handles the following:
HTML Page Caching
- Caches HTML responses with a default TTL (time-to-live) of 10 minutes — this can be customized in the /sw.js file.
If you need to change the location of the service worker file, you can pass a custom path via a prop in PWAProvider:
import { PWAProvider } from "next-pwa-pack";
export default function layout({ children }) {
return <PWAProvider swPath="/some-path/sw.js">{children}</PWAProvider>
;
}
- Automatically updates the cache when the TTL expires;
- Serves the cached version when the network is unavailable.
Static Asset Caching
- CSS, JavaScript, and image files are cached indefinitely;
- Improves load speed on repeat visits;
- Applies only to GET requests (for security reasons).
Messages Processing
The service worker listens for six types of messages from the client:
- CACHE_CURRENT_HTML – cache the current page;
- REVALIDATE_URL – force cache update for a specific URL;
- DISABLE_CACHE / ENABLE_CACHE – toggle caching on or off;
- SKIP_WAITING – activate the new version of the service worker immediately;
- CLEAR_STATIC_CACHE – clear static asset and API response caches (e.g. after server-side revalidation or an SSE-driven update).
Offline Mode
- Automatically serves offline.html when there’s no internet connection and the current page isn’t cached;
- Attempts to fetch a fresh version once the connection is restored.
Integration with SSR/Edge middleware. Supports server actions and SSE-based cache updates. The server can send revalidation events, which are handled by the client to update the cache accordingly.
HOC withPWA
Integration with SSR/Edge middleware. Supports server actions and SSE-based cache updates. The server can send revalidation events, which are handled by the client to update the cache accordingly.
export default withPWA(originalMiddleware, {
revalidationSecret: process.env.REVALIDATION_SECRET!,
sseEndpoint: "/api/pwa/cache-events",
webhookPath: "/api/pwa/revalidate",
});
- `originalMiddleware` — your existing middleware function (e.g. for i18n, authentication, etc.)
- `revalidationSecret` — a secret token used to authorize revalidation requests, so only you can trigger them
- `sseEndpoint` — (defaults to `/api/pwa/cache-events`). Change it if this route is already used in your app
- `webhookPath` — the endpoint for webhook-based revalidation (defaults to `/api/pwa/revalidate`). Accepts POST requests for triggering cache revalidation from the server or external sources; also used internally by the revalidatePWA function
Usage Examples
Here are a few real-world scenarios where the package comes into play.
Updating the Cache After Data Changes
import { updateSWCache } from "next-pwa-pack";
// After successfully creating a post
const handleCreatePost = async (data) => {
await createPost(data);
// Update cache for tabs with the blog and dashboard
updateSWCache(["/blog", "/dashboard"]);
};
If you need to trigger a cache update from the server:
import { revalidatePWA } from "../actions";
await createPost(data);
await revalidatePWA(["/my-page"]);
Clearing the cache on user logout
import { clearAllCache } from "next-pwa-pack";
const handleLogout = async () => {
await logout();
await clearAllCache(); // Clear all caches
router.push("/login");
};
Quick Description of All Exported Client-Side Actions
import {
clearAllCache,
reloadServiceWorker,
updatePageCache,
unregisterServiceWorkerAndClearCache,
updateSWCache,
disablePWACache,
enablePWACache,
clearStaticCache,
usePWAStatus,
} from "next-pwa-pack";
// Clears all caches managed by the service worker.
await clearAllCache();
// Reloads the service worker and refreshes the page.
await reloadServiceWorker();
// Updates the cache for a specific page (or the current one if no URL is provided).
await updatePageCache("/about");
// Unregisters the service worker and clears all caches.
await unregisterServiceWorkerAndClearCache();
// Broadcasts a cache update signal to all open tabs and updates the current one.
// Can be used after revalidateTag on the client.
// Clears static asset and API response caches.
await clearStaticCache();
updateSWCache(["/page1", "/page2"]);
// Globally disables PWA caching (until page reload or calling enablePWACache).
disablePWACache();
// Globally disables PWA caching (until calling disablePWACache).
enablePWACache();
const { online, hasUpdate, swInstalled, update } = usePWAStatus();
// - `online` — current online/offline status
// - `hasUpdate` — whether a new version is available
// - `swInstalled` — whether the service worker is installed
// - `update()` — activates the new version of the app
Example: API Route for External Revalidation
Sometimes, you need to update the cache externally right after a response changes instead of waiting for the cache TTL to expire. Here is one common example when data is modified through an admin panel.
To handle this situation, you can create an API route like this:
// 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";
interface RevalidateRequest {
tags?: string[];
secret: string;
urls?: string[];
}
interface RevalidateResponse {
success: boolean;
message: string;
tagsRevalidated: boolean;
urlsRevalidated: boolean;
tags: string[];
urls: string[];
successful: number;
failed: number;
timestamp: string;
}
export async function POST(request: NextRequest) {
try {
const { tags, secret, urls }: RevalidateRequest = await request.json();
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let successful = 0;
let failed = 0;
let tagsRevalidated = false;
let urlsRevalidated = false;
const validTags = Object.values(FetchTags);
const invalidTags =
tags?.filter((tag) => !validTags.includes(tag as any)) || [];
if (invalidTags.length > 0) {
return NextResponse.json(
{ error: `Invalid tags: ${invalidTags.join(", ")}` },
{ status: 400 }
);
}
if (tags && tags.length > 0) {
const tagResults = await Promise.allSettled(
tags.map((tag) => revalidateTag(tag as FetchTags))
);
successful = tagResults.filter((r) => r.status === "fulfilled").length;
failed = tagResults.filter((r) => r.status === "rejected").length;
tagsRevalidated = true;
}
if (urls && urls.length > 0) {
await revalidatePWA(urls);
urlsRevalidated = true;
}
const response: RevalidateResponse = {
success: true,
message: "Cache revalidation completed",
tagsRevalidated,
urlsRevalidated,
tags: tags || [],
urls: urls || [],
successful,
failed,
timestamp: new Date().toISOString(),
};
return NextResponse.json(response);
} catch (error) {
console.error("Webhook revalidation error:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Now your app can receive external revalidation requests. For example:
POST: https://my-app(или localhost:3000)/api/webhook/revalidate
body: {
"tags": ["faq"],
"secret": "1234567890",
"urls": ["/ru/question-answer"]
}
response: {
"success": true,
"message": "Cache revalidation completed",
"tagsRevalidated": true,
"urlsRevalidated": true,
"tags": [
"faq"
],
"urls": [
"/ru/question-answer"
],
"successful": 1,
"failed": 0,
"timestamp": "2025-07-21T12:43:47.819Z"
}
Debugging & Monitoring
Here’s what happens at this stage.
Verifying Cache Behavior
- Open DevTools → Application → Service Workers.
- Make sure the service worker is registered.
- Go to Cache Storage → html-cache-v2.
- Confirm that pages are being cached.
Testing Offline Mode
- Enable devMode in PWAProvider.
- Open the developer panel (look for the green dot in the bottom-left corner).
- Go to DevTools → Network → Offline.
- Refresh the page — you should see the offline.html fallback page.
Console Logs
The service worker provides detailed logging in the browser console:
- [PWA] Service Worker registered – successful registration;
- [SW] Cached: /about – page cached;
- [SW] Revalidated and updated cache for: /blog – cache updated.
Limitations and Package Specifics
A few important things to keep in mind before installing the package.
Security
- HTTPS is required for PWA functionality in production;
- Only GET requests are cached (API calls are not cached);
- Sensitive data is never stored in the cache.
Performance
- The package has no impact on performance under normal conditions;
- It improves page load speed on repeat visits.
Configuration
- Cache TTL (default: 10 minutes) can only be changed in sw.js;
- Cache exclusions are set via CACHE_EXCLUDE;
- manifest.json must be manually configured for your project;
- The revalidatePWA server action is copied into your project and can be customized;
- The withPWA HOC accepts props for advanced configuration;
- PWAProvider also includes configurable props for controlling behavior:
export default function PWAProvider({
children,
swPath,
devMode = false,
serverRevalidation = { enabled: true, sseEndpoint: "/api/pwa/cache-events" },
}: PWAProviderProps) {
Final Thoughts
next-pwa-pack is a fast and developer-friendly way to turn your Next.js app into a fully functional PWA. It takes care of all the heavy lifting around service worker registration, caching, and offline support — and gives you a simple API for managing everything.
In future releases, we plan to add:
- Configurable TTL (no need to edit sw.js manually);
- Push notification support;
- More flexible cache rules (e.g. by URL patterns);
- Performance metrics to help monitor caching efficiency.
The package was built for Next.js 15 and hasn’t been fully tested on earlier versions — but it should work fine with the Next.js 13 App Router.
Thanks for reading! 🙂