Progressive Web Apps in 2025: The Complete Implementation Guide

Build production-ready PWAs with modern APIs. Covers service workers, Web Push, offline support, app manifest, install prompts, background sync, and...

Y
Yash Pritwani
14 min read

Why PWAs in 2025?

Progressive Web Apps have matured significantly. With iOS finally improving PWA support and new APIs like Background Sync, Web Push (on iOS since 16.4), and the File System Access API, PWAs can now replace native apps for most use cases.

docker-compose.yml123456789version: "3.8"services: web: image: nginx:alpine ports: - "80:80" volumes: - ./html:/usr/share/nginx

A well-structured configuration file is the foundation of reproducible infrastructure.

Benefits:

  • One codebase for web, Android, iOS, and desktop
  • No app store approval process
  • Instant updates without user action
  • Smaller footprint than native apps
  • Works offline with service workers
  • Installable on home screen

The Manifest

The web app manifest defines how your PWA appears when installed:

// public/manifest.json
{
  "name": "TechSaaS Platform",
  "short_name": "TechSaaS",
  "description": "Self-hosted infrastructure management",
  "start_url": "/dashboard",
  "display": "standalone",
  "background_color": "#0f172a",
  "theme_color": "#3b82f6",
  "orientation": "any",
  "scope": "/",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-maskable-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable"
    }
  ],
  "screenshots": [
    {
      "src": "/screenshots/desktop.png",
      "sizes": "1280x720",
      "type": "image/png",
      "form_factor": "wide"
    },
    {
      "src": "/screenshots/mobile.png",
      "sizes": "750x1334",
      "type": "image/png",
      "form_factor": "narrow"
    }
  ]
}

Get more insights on Tutorials

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

Link it in your HTML:

<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#3b82f6">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

Service Worker

The service worker handles caching, offline support, and background tasks:

// public/sw.js
const CACHE_NAME = "techsaas-v1";
const STATIC_ASSETS = [
  "/",
  "/dashboard",
  "/offline",
  "/icons/icon-192.png",
  "/icons/icon-512.png",
];

// Install — cache static assets
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS);
    })
  );
  self.skipWaiting();
});

// Activate — clean up old caches
self.addEventListener("activate", (event) => {
  event.waitUntil(
    caches.keys().then((keys) => {
      return Promise.all(
        keys
          .filter((key) => key !== CACHE_NAME)
          .map((key) => caches.delete(key))
      );
    })
  );
  self.clients.claim();
});

// Fetch — network first, cache fallback
self.addEventListener("fetch", (event) => {
  // Skip non-GET requests
  if (event.request.method !== "GET") return;

  // API requests — network only
  if (event.request.url.includes("/api/")) {
    event.respondWith(
      fetch(event.request).catch(() => {
        return new Response(JSON.stringify({ error: "Offline" }), {
          headers: { "Content-Type": "application/json" },
          status: 503,
        });
      })
    );
    return;
  }

  // Static assets — cache first
  if (event.request.url.match(/\.(js|css|png|jpg|svg|woff2)$/)) {
    event.respondWith(
      caches.match(event.request).then((cached) => {
        return cached || fetch(event.request).then((response) => {
          const clone = response.clone();
          caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
          return response;
        });
      })
    );
    return;
  }

  // Pages — network first, cache fallback
  event.respondWith(
    fetch(event.request)
      .then((response) => {
        const clone = response.clone();
        caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
        return response;
      })
      .catch(() => {
        return caches.match(event.request).then((cached) => {
          return cached || caches.match("/offline");
        });
      })
  );
});

Registration

// src/lib/register-sw.ts
export async function registerServiceWorker() {
  if ("serviceWorker" in navigator) {
    try {
      const registration = await navigator.serviceWorker.register("/sw.js", {
        scope: "/",
      });

      // Check for updates periodically
      setInterval(() => {
        registration.update();
      }, 60 * 60 * 1000); // Every hour

      console.log("SW registered:", registration.scope);
    } catch (error) {
      console.error("SW registration failed:", error);
    }
  }
}
Terminal$docker compose up -d[+] Running 5/5Network app_default CreatedContainer web StartedContainer api StartedContainer db Started$

Docker Compose brings up your entire stack with a single command.

Web Push Notifications

// Request permission and subscribe
async function subscribeToPush() {
  const permission = await Notification.requestPermission();
  if (permission !== "granted") return;

  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
  });

  // Send subscription to your server
  await fetch("/api/push/subscribe", {
    method: "POST",
    body: JSON.stringify(subscription),
    headers: { "Content-Type": "application/json" },
  });
}

Handle push events in the service worker:

// In sw.js
self.addEventListener("push", (event) => {
  const data = event.data?.json() || {};

  event.waitUntil(
    self.registration.showNotification(data.title || "Notification", {
      body: data.body,
      icon: "/icons/icon-192.png",
      badge: "/icons/badge-72.png",
      data: { url: data.url || "/" },
      actions: data.actions || [],
    })
  );
});

self.addEventListener("notificationclick", (event) => {
  event.notification.close();
  const url = event.notification.data.url;
  event.waitUntil(clients.openWindow(url));
});

Install Prompt

"use client";

import { useState, useEffect } from "react";

export function InstallPrompt() {
  const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
  const [showInstall, setShowInstall] = useState(false);

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault();
      setDeferredPrompt(e);
      setShowInstall(true);
    };

    window.addEventListener("beforeinstallprompt", handler);
    return () => window.removeEventListener("beforeinstallprompt", handler);
  }, []);

  const handleInstall = async () => {
    if (!deferredPrompt) return;
    deferredPrompt.prompt();
    const { outcome } = await deferredPrompt.userChoice;
    console.log("Install outcome:", outcome);
    setShowInstall(false);
    setDeferredPrompt(null);
  };

  if (!showInstall) return null;

  return (
    <div className="install-banner">
      <p>Install TechSaaS for a better experience</p>
      <button 
      <button  => setShowInstall(false)}>Dismiss</button>
    </div>
  );
}

Background Sync

Free Resource

Free Cloud Architecture Checklist

A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.

Download the Checklist

Queue actions when offline and sync when back online:

// In your app code
async function saveData(data) {
  try {
    await fetch("/api/save", {
      method: "POST",
      body: JSON.stringify(data),
    });
  } catch (error) {
    // Queue for background sync
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register("sync-data");

    // Store data in IndexedDB for the service worker to use
    const db = await openDB("sync-queue");
    await db.add("pending", data);
  }
}

// In sw.js
self.addEventListener("sync", (event) => {
  if (event.tag === "sync-data") {
    event.waitUntil(syncPendingData());
  }
});

async function syncPendingData() {
  const db = await openDB("sync-queue");
  const pending = await db.getAll("pending");

  for (const item of pending) {
    await fetch("/api/save", {
      method: "POST",
      body: JSON.stringify(item),
    });
    await db.delete("pending", item.id);
  }
}

PWA Checklist

  • Web app manifest with icons (192px and 512px)
  • Service worker with offline support
  • HTTPS everywhere
  • Responsive design
  • Fast loading (LCP under 2.5s)
  • Works without JavaScript (graceful degradation)
  • Meta theme-color tag
  • Apple touch icon
  • Offline fallback page
docker-compose.ymlWeb AppAPI ServerDatabaseCacheDocker Network:3000:8080:5432:6379

Docker Compose defines your entire application stack in a single YAML file.

PWA vs Native in 2025

Capability PWA Native
Push Notifications Yes (including iOS) Yes
Offline Support Yes Yes
Camera Access Yes Yes
GPS/Location Yes Yes
File System Partial (OPFS) Full
Bluetooth Yes (Web Bluetooth) Yes
Background Processing Limited Full
App Store Distribution No (direct install) Yes
Auto-Updates Yes Manual
Install Size <1MB typically 50-200MB

At TechSaaS, we build PWAs for clients who want cross-platform apps without the cost of maintaining separate iOS, Android, and web codebases. For most business applications, PWAs provide 95% of native functionality at a fraction of the development cost.

Need help building a progressive web app? Contact [email protected].

#pwa#service-worker#web-push#offline#frontend#mobile

Related Service

Cloud Solutions

Let our experts help you build the right technology strategy for your business.

Need help with tutorials?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.