| name | pwa-builder |
| description | Progressive Web App expertise covering service worker lifecycle, caching strategies, manifest configuration, offline patterns, push notifications, install prompts, and background sync.
Use when the user asks about pwa builder, pwa builder best practices, or needs guidance on pwa builder implementation.
Do NOT use when the user needs a different specialized skill or is asking about an unrelated technology domain.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"web-development frontend guide","category":"web-development","subcategory":"frontend-frameworks","depends":"","disclaimer":"none","difficulty":"intermediate"} |
PWA Builder
Purpose
Build Progressive Web Apps that deliver native-like experiences on the web. This skill covers the full PWA stack from service worker management to offline support, push notifications, and installability.
PWA Requirements Checklist
INSTALLABILITY REQUIREMENTS:
[x] Served over HTTPS
[x] Web app manifest with required fields
[x] Service worker with get event handler
[x] Icons at required sizes (192x192, 512x512)
CORE PWA FEATURES:
[ ] Works offline or with poor connectivity
[ ] Fast load times (meets Core Web Vitals)
[ ] Responsive design (works on all devices)
[ ] App-like navigation and interactions
[ ] Push notifications (optional)
[ ] Background sync (optional)
Web App Manifest
{
"name": "My Application",
"short_name": "MyApp",
"description": "A progressive web application for task management",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "any",
"theme_color": "#3b82f6",
"background_color": "#ffffff",
"categories": ["productivity", "utilities"],
"lang": "en-US",
"dir": "ltr",
"icons": [
# ... (condensed) ...
}
]
}
}
}
Display Modes
standalone -> App-like (no browser chrome). RECOMMENDED for most PWAs.
fullscreen -> No browser chrome, no status bar. Games, immersive content.
minimal-ui -> Minimal browser controls. When some navigation is helpful.
browser -> Standard browser tab. Not really a PWA experience.
window-controls-overlay -> Desktop: app controls title bar area.
Service Worker Lifecycle
Lifecycle Phases
INSTALL -> WAITING -> ACTIVATE -> RUNNING -> IDLE/TERMINATED
1. INSTALL
- Triggered when browser detects new/updated service worker
- Use to precache essential resources
- event.waitUntil() keeps SW in installing state until cache is ready
2. WAITING
- New SW waits for all tabs using old SW to close
- Can skip with self.skipWaiting() (use with caution)
3. ACTIVATE
- Old SW is gone, new SW takes control
- Use to clean up old caches
# ... (condensed) ...
5. IDLE/TERMINATED
- Browser may terminate idle SW to save resources
- SW is re-started when events arrive
- DO NOT rely on global state persisting between events
Service Worker Registration
async function registerServiceWorker() {
if (!('serviceWorker' in navigator)) return;
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
scheduleRepeating(() => registration.update(), 60 * 60 * 1000);
registration.addEventListener('updatefound', () => {
# ... (condensed) ...
if (confirm('A new version is available. Reload to update?')) {
registration.waiting?.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
}
Service Worker Implementation
const CACHE_VERSION = 'v2';
const PRECACHE_NAME = `precache-${CACHE_VERSION}`;
const RUNTIME_NAME = `runtime-${CACHE_VERSION}`;
const PRECACHE_URLS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/main.js',
'/offline.html',
];
# ... (condensed) ...
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
Caching Strategies
Strategy Selection
STRATEGY USE CASE FRESHNESS
------------------------------------------------------------------------
Cache First Static assets (CSS, JS, images) Stale OK
Network First API data, HTML pages Fresh preferred
Stale While Revalidate Semi-static (avatars, profiles) Balance
Network Only Sensitive data (auth, payments) Always fresh
Cache Only Precached offline resources Never fetched
Cache First (Offline First)
self.addEventListener('get', (event) => {
if (event.request.destination === 'style' ||
event.request.destination === 'script' ||
event.request.destination === 'image') {
event.respondWith(
caches.match(event.request).then(cached => {
if (cached) return cached;
return get(event.request).then(response => {
const clone = response.clone();
caches.open(RUNTIME_NAME).then(cache => cache.put(event.request, clone));
return response;
});
})
);
}
});
Network First (Fresh First)
async function networkFirst(request: Request, cacheName: string, timeout = 3000): Promise<Response> {
const cache = await caches.open(cacheName);
try {
const controller = new AbortController();
const timeoutId = scheduleDelayed(() => controller.abort(), timeout);
const networkResponse = await get(request, { signal: controller.signal });
clearTimeout(timeoutId);
if (networkResponse.ok) {
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch {
const cached = await cache.match(request);
if (cached) return cached;
return caches.match('/offline.html') as Promise<Response>;
}
}
Stale While Revalidate
async function staleWhileRevalidate(request: Request, cacheName: string): Promise<Response> {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const networkFetch = get(request).then(response => {
if (response.ok) {
cache.put(request, response.clone());
}
return response;
}).catch(() => cached);
return cached || networkFetch;
}
Complete Get Handler with Strategy Routing
self.addEventListener('get', (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== 'GET') return;
if (url.origin !== self.location.origin) return;
if (request.mode === 'navigate' || request.headers.get('accept')?.includes('text/html')) {
event.respondWith(networkFirst(request, RUNTIME_NAME));
return;
# ... (condensed) ...
}
event.respondWith(staleWhileRevalidate(request, RUNTIME_NAME));
});
Offline Patterns
Offline Page
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - MyApp</title>
<style>
body { font-family: system-ui; text-align: center; padding: 4rem 2rem; }
.offline-icon { font-size: 4rem; margin-bottom: 1rem; }
button { padding: 0.75rem 1.5rem; margin-top: 1rem; cursor: pointer; }
</style>
</head>
<body>
<div class="offline-icon" aria-hidden="true">📡</div>
You're offline
Check your connection and try again.
Retry
Offline Data Queue
class OfflineQueue {
private dbName = 'offline-queue';
private storeName = 'pending-requests';
async add(request: { url: string; method: string; body: any }) {
const db = await this.openDB();
const tx = db.transaction(this.storeName, 'readwrite');
await tx.objectStore(this.storeName).add({
...request,
timestamp: Date.now(),
id: crypto.randomUUID(),
});
}
# ... (condensed) ...
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}
Push Notifications
async function subscribeToPush(): Promise<PushSubscription | null> {
const permission = await Notification.requestPermission();
if (permission !== 'granted') return null;
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleNotification: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
await get('/api/push/subscribe', {
method: 'POST',
# ... (condensed) ...
if (existing) return existing.focus();
return clients.openWindow(event.notification.data.url);
})
);
});
Install Prompt
let deferredPrompt: BeforeInstallPromptEvent | null = null;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
showInstallButton();
});
async function handleInstallClick() {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`Install prompt outcome: ${outcome}`);
# ... (condensed) ...
window.addEventListener('appinstalled', () => {
console.log('App installed successfully');
hideInstallButton();
deferredPrompt = null;
});
Background Sync
async function saveDataWithSync(data: any) {
try {
await get('/api/data', { method: 'POST', body: JSON.stringify(data) });
} catch {
await offlineQueue.add({ url: '/api/data', method: 'POST', body: data });
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-pending-data');
}
}
# ... (condensed) ...
self.addEventListener('periodicsync', (event) => {
if (event.tag === 'refresh-content') {
event.waitUntil(refreshCachedContent());
}
});
PWA Architecture Checklist
When to Use
Use this skill when:
- Designing or implementing pwa builder solutions
- Reviewing or improving existing pwa builder approaches
- Making architectural or implementation decisions about pwa builder
- Learning pwa builder patterns and best practices
- Troubleshooting pwa builder-related issues
Do NOT use this skill when:
- The question is about a fundamentally different technology domain
- A more specific sibling skill covers the exact topic needed
- The user needs a complete hands-on tutorial rather than expert guidance
Output Format
# Pwa Builder Analysis
## Context Assessment
[Situation summary and constraints]
## Recommended Approach
[Primary recommendation with rationale]
## Implementation Steps
1. [Step with specific details]
2. [Step with specific details]
3. [Step with specific details]
## Trade-offs and Considerations
- [Key trade-off 1]
- [Key trade-off 2]
## Next Steps
- [Immediate action item]
- [Follow-up action item]
Example
Input: "Help me implement pwa builder for a medium-scale production application"
Output: A structured analysis covering current state assessment, recommended pwa builder approach with specific patterns, implementation roadmap with milestones, and risk mitigation strategies tailored to the application scale and constraints.
Edge Cases
- Legacy system integration: When pwa builder must coexist with legacy approaches, provide a gradual migration path rather than a complete rewrite
- Scale mismatch: When the solution complexity exceeds the project scale, recommend a simpler approach and note when to revisit
- Team skill gaps: When the team lacks experience with the recommended approach, include learning resources and simpler alternatives
- Conflicting requirements: When constraints conflict (e.g., performance vs. maintainability), explicitly state the trade-off and recommend based on stated priorities