Skip to main content
โšก Calmops

PWA for Startups: Progressive Web Apps That Feel Native

Introduction

Progressive Web Apps (PWAs) give you mobile app capabilities without the app store. Users can install them on their home screen, work offline, and receive push notifications. For startups, PWAs offer a fast path to mobile users without the overhead of native apps.


Why PWA?

Benefits

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    PWA Benefits                               โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                             โ”‚
โ”‚  โœ“ No app store approval needed                           โ”‚
โ”‚  โœ“ Instant updates (no App Store review)                  โ”‚
โ”‚  โœ“ Installable on home screen                            โ”‚
โ”‚  โœ“ Works offline                                          โ”‚
โ”‚  โœ“ Push notifications                                     โ”‚
โ”‚  โœ“ Native-like experience                                โ”‚
โ”‚  โœ“ Lower development cost                                 โ”‚
โ”‚  โœ“ Single codebase for web + mobile                       โ”‚
โ”‚                                                             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

PWA vs Native App

# Comparison
pwa:
  development: "Single web codebase"
  distribution: "Web (no store)"
  updates: "Instant"
  cost: "One team"
  offline: "Yes (service workers)"
  push_notifications: "Yes"
  app_store: "No"
  
native:
  development: "iOS + Android = 2 teams"
  distribution: "App Store / Play Store"
  updates: "App Store review (days)"
  cost: "2-3x more"
  offline: "Yes"
  push_notifications: "Yes"
  app_store: "Yes"

Building a PWA

Web App Manifest

// public/manifest.json
{
  "name": "My Startup App",
  "short_name": "Startup",
  "description": "The best startup app",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#000000",
  "icons": [
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
// app/layout.tsx - Add manifest link
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link rel="manifest" href="/manifest.json" />
        <meta name="theme-color" content="#000000" />
        <link rel="apple-touch-icon" href="/icons/icon-192.png" />
      </head>
      <body>{children}</body>
    </html>
  )
}

Service Worker

// public/sw.ts (Next.js 13+)
/// <reference lib="webworker" />

const CACHE_NAME = 'my-startup-v1'
const STATIC_ASSETS = [
  '/',
  '/manifest.json',
  '/icons/icon-192.png',
  '/icons/icon-512.png',
]

declare const self: ServiceWorkerGlobalScope

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

// Fetch: serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
  // Skip non-GET requests
  if (event.request.method !== 'GET') return

  event.respondWith(
    caches.match(event.request).then((cached) => {
      // Return cached or fetch
      const networked = fetch(event.request)
        .then((response) => {
          // Cache new requests
          const cacheCopy = response.clone()
          caches.open(CACHE_NAME).then((cache) => {
            cache.put(event.request, cacheCopy)
          })
          return response
        })
        .catch(() => {
          // Offline fallback
          return caches.match('/offline')
        })

      return cached || networked
    })
  )
})

// Activate: clean 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))
      )
    })
  )
})

Register Service Worker

// components/RegisterSW.tsx
'use client'

import { useEffect } from 'react'

export function ServiceWorkerRegistration() {
  useEffect(() => {
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker
        .register('/sw.js')
        .then((registration) => {
          console.log('SW registered:', registration)
        })
        .catch((error) => {
          console.log('SW registration failed:', error)
        })
    }
  }, [])

  return null
}

PWA Features

Offline Support

// Detect offline status
export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true)

  useEffect(() => {
    setIsOnline(navigator.onLine)

    const handleOnline = () => setIsOnline(true)
    const handleOffline = () => setIsOnline(false)

    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)

    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])

  return isOnline
}

Push Notifications

// Request notification permission
async function requestNotificationPermission() {
  if (!('Notification' in window)) {
    console.log('Notifications not supported')
    return
  }

  const permission = await Notification.requestPermission()
  
  if (permission === 'granted') {
    console.log('Notification permission granted')
    // Subscribe to push service
    await subscribeToPush()
  }
}

// Subscribe to push notifications
async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(
      process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!
    ),
  })

  // Send subscription to server
  await fetch('/api/push/subscribe', {
    method: 'POST',
    body: JSON.stringify(subscription),
  })
}

Next.js PWA Plugin

// next.config.js with PWA
const withPWA = require('@ducanh2912/next-pwa').default({
  dest: 'public',
  disable: process.env.NODE_ENV === 'development',
})

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Your config
}

module.exports = withPWA(nextConfig)

Cost Analysis

# PWA vs Native App cost comparison
pwa:
  development_time: "1-2 months"
  development_cost: "$5,000-15,000"
  app_store: "No"
  updates: "Instant"
  maintenance: "Single codebase"
  
native_ios:
  development_time: "2-4 months"
  development_cost: "$15,000-30,000"
  app_store: "Yes (7+ days review)"
  updates: "App Store review"
  maintenance: "iOS team"
  
native_android:
  development_time: "2-4 months"
  development_cost: "$15,000-30,000"
  app_store: "Yes (hours to days)"
  updates: "Play Store review"
  maintenance: "Android team"

Key Takeaways

  • PWAs are free - No app store fees
  • Instant updates - No review process
  • Works offline - Service workers
  • Mobile reach - Add to home screen

External Resources

Comments