Summary
Install and configure [Serwist](https://serwist.pages.dev/) as the PWA/service worker solution for apps/dash (Next.js 16 App Router) using the Turbopack integration (@serwist/turbopack). This enables installability, offline shell caching, and PWA manifest integration.
Note: Next.js 16 uses Turbopack by default. Use @serwist/turbopack — NOT @serwist/next (which is webpack-only).
Background
Domus is a mobile-first PWA targeting parishioners in East Kalimantan, many of whom are on low-bandwidth connections. Serwist is the chosen PWA library (see docs/tdd.md — Tech Stack). It needs to be properly installed and wired into the Next.js 16 App Router + Turbopack setup.
Acceptance Criteria
Implementation Guide
1. Install dependencies
pnpm --filter @domus/dash add -D @serwist/turbopack esbuild serwist
Do NOT install @serwist/next — that is the webpack variant.
2. Update next.config.ts
// apps/dash/next.config.ts
import { withSerwist } from "@serwist/turbopack";
const nextConfig = {
// your existing Next.js config here
};
export default withSerwist(nextConfig);
3. Create the Serwist Route Handler
This is how @serwist/turbopack serves the compiled service worker — via a Next.js Route Handler instead of a static file.
// apps/dash/app/serwist/[path]/route.ts
import { spawnSync } from "node:child_process";
import { createSerwistRoute } from "@serwist/turbopack";
const revision =
spawnSync("git", ["rev-parse", "HEAD"], { encoding: "utf-8" }).stdout ??
crypto.randomUUID();
export const {
dynamic,
dynamicParams,
revalidate,
generateStaticParams,
GET,
} = createSerwistRoute({
additionalPrecacheEntries: [{ url: "/~offline", revision }],
swSrc: "app/sw.ts",
useNativeEsbuild: true,
});
4. Create the service worker
// apps/dash/app/sw.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="webworker" />
import { defaultCache } from "@serwist/turbopack/worker";
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
import { Serwist } from "serwist";
declare global {
interface WorkerGlobalScope extends SerwistGlobalConfig {
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
}
}
declare const self: ServiceWorkerGlobalScope;
const serwist = new Serwist({
precacheEntries: self.__SW_MANIFEST,
skipWaiting: true,
clientsClaim: true,
navigationPreload: true,
runtimeCaching: defaultCache,
fallbacks: {
entries: [
{
url: "/~offline",
matcher({ request }) {
return request.destination === "document";
},
},
],
},
});
serwist.addEventListeners();
5. Create the offline fallback page
// apps/dash/app/~offline/page.tsx
export default function OfflinePage() {
return (
<div>
<h1>Tidak ada koneksi internet</h1>
<p>Harap periksa koneksi Anda dan coba lagi.</p>
</div>
);
}
6. Update app/layout.tsx
Create a separate client file for SerwistProvider (required because it's a Client Component):
// apps/dash/app/serwist.ts
"use client";
export { SerwistProvider } from "@serwist/turbopack/react";
Then update app/layout.tsx:
// apps/dash/app/layout.tsx
import type { Metadata, Viewport } from "next";
import type { ReactNode } from "react";
import { SerwistProvider } from "./serwist";
export const metadata: Metadata = {
applicationName: "Domus",
title: {
default: "Domus — Paroki Kristus Raja Barong Tongkok",
template: "%s — Domus",
},
description: "Aplikasi administrasi digital Paroki Kristus Raja Barong Tongkok",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "Domus",
},
formatDetection: {
telephone: false,
},
};
export const viewport: Viewport = {
themeColor: "#1B3A6B",
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="id">
<head>
<link rel="icon" type="image/x-icon" href="/icons/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
</head>
<body>
<SerwistProvider swUrl="/serwist/sw.js">{children}</SerwistProvider>
</body>
</html>
);
}
7. Create app/manifest.json
Place in the App Router root so Next.js serves it automatically at /manifest.json:
{
"name": "Domus — Paroki Kristus Raja Barong Tongkok",
"short_name": "Domus",
"description": "Aplikasi administrasi digital Paroki Kristus Raja Barong Tongkok",
"start_url": "/",
"display": "standalone",
"background_color": "#1B3A6B",
"theme_color": "#1B3A6B",
"orientation": "portrait",
"icons": [
{
"src": "/icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "/icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/icons/icon-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/icons/icon-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
8. Add PWA icons to public/
apps/dash/public/
└── icons/
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-48x48.png
├── icon-192x192.png
├── icon-512x512.png
├── icon-maskable-192x192.png
├── icon-maskable-512x512.png
└── apple-touch-icon.png
All files are available in domus-favicon.zip from the design assets.
9. Update .gitignore
No generated service worker file to ignore — @serwist/turbopack serves the SW via Route Handler, not a static file in public/. Nothing to add here.
File Structure Summary
apps/dash/
├── app/
│ ├── manifest.json ← NEW: PWA manifest
│ ├── serwist.ts ← NEW: SerwistProvider re-export (client)
│ ├── sw.ts ← NEW: Service worker source
│ ├── layout.tsx ← UPDATED: metadata, viewport, SerwistProvider
│ ├── serwist/
│ │ └── [path]/
│ │ └── route.ts ← NEW: Serwist Route Handler
│ └── ~offline/
│ └── page.tsx ← NEW: Offline fallback page
├── public/
│ └── icons/ ← NEW: All favicon & PWA icon files
└── next.config.ts ← UPDATED: wrapped with withSerwist
Verification Steps
pnpm build — no errors
pnpm start
- Open Chrome DevTools → Application → Service Workers — confirm SW is registered at
/serwist/sw.js
- Application → Manifest — confirm manifest is valid and icons load
- Run Lighthouse audit → PWA section should pass installable criteria
- Test "Add to Home Screen" on Android Chrome and iOS Safari
- Disable network in DevTools → navigate to any page →
/~offline fallback should appear
References
Labels: infra phase-2 status: todo
Milestone: Phase 2 — Auth & Shell
Summary
Install and configure [Serwist](https://serwist.pages.dev/) as the PWA/service worker solution for
apps/dash(Next.js 16 App Router) using the Turbopack integration (@serwist/turbopack). This enables installability, offline shell caching, and PWA manifest integration.Background
Domus is a mobile-first PWA targeting parishioners in East Kalimantan, many of whom are on low-bandwidth connections. Serwist is the chosen PWA library (see
docs/tdd.md— Tech Stack). It needs to be properly installed and wired into the Next.js 16 App Router + Turbopack setup.Acceptance Criteria
@serwist/turbopack,esbuild, andserwistinstalled as dev dependencies inapps/dashnext.config.tswrapped withwithSerwistfrom@serwist/turbopackapp/serwist/[path]/route.tsapp/sw.tsapp/~offline/page.tsxSerwistProvideradded toapp/layout.tsxapp/layout.tsxapp/manifest.jsonpresent and valid (name, short_name, theme_color#1B3A6B, icons array)apps/dash/public/icons/(see design assets)pnpm typecheckpasses)pnpm buildpasses)Implementation Guide
1. Install dependencies
2. Update
next.config.ts3. Create the Serwist Route Handler
This is how
@serwist/turbopackserves the compiled service worker — via a Next.js Route Handler instead of a static file.4. Create the service worker
5. Create the offline fallback page
6. Update
app/layout.tsxCreate a separate client file for
SerwistProvider(required because it's a Client Component):Then update
app/layout.tsx:7. Create
app/manifest.jsonPlace in the App Router root so Next.js serves it automatically at
/manifest.json:{ "name": "Domus — Paroki Kristus Raja Barong Tongkok", "short_name": "Domus", "description": "Aplikasi administrasi digital Paroki Kristus Raja Barong Tongkok", "start_url": "/", "display": "standalone", "background_color": "#1B3A6B", "theme_color": "#1B3A6B", "orientation": "portrait", "icons": [ { "src": "/icons/favicon-16x16.png", "sizes": "16x16", "type": "image/png" }, { "src": "/icons/favicon-32x32.png", "sizes": "32x32", "type": "image/png" }, { "src": "/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }, { "src": "/icons/icon-maskable-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" }, { "src": "/icons/icon-maskable-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } ] }8. Add PWA icons to
public/All files are available in
domus-favicon.zipfrom the design assets.9. Update
.gitignoreNo generated service worker file to ignore —
@serwist/turbopackserves the SW via Route Handler, not a static file inpublic/. Nothing to add here.File Structure Summary
Verification Steps
pnpm build— no errorspnpm start/serwist/sw.js/~offlinefallback should appearReferences
docs/tdd.md— Tech Stack section (Serwist listed as PWA solution)domus-favicon.zip(icons + manifest)Labels:
infraphase-2status: todoMilestone: Phase 2 — Auth & Shell