Guide to PWA with Laravel + Inertia + React

Posted on

This comprehensive guide explains what PWAs are, how they work, and how to implement them step-by-step in your Laravel application. We’ll cover every concept from the ground up.

Table of Contents

1. Understanding Progressive Web Apps

What is a Progressive Web App?

A Progressive Web App (PWA) is a web application that uses modern web technologies to provide a native app-like experience to users. Think of it as a bridge between traditional websites and mobile apps.

Key characteristics of PWAs:

  • Installable: Users can install them on their home screen like native apps
  • Offline-capable: They work even without internet connection
  • App-like: They feel like native mobile apps with full-screen experience
  • Cross-platform: One codebase works on all devices and operating systems
  • Always up-to-date: Updates happen automatically through the web

Why Use PWAs?

For Users:

  • No app store required – install directly from the web
  • Takes less storage space than native apps
  • Always has the latest version
  • Works on any device with a modern browser

For Developers:

  • One codebase for all platforms
  • Easier to maintain than separate native apps
  • Can leverage existing web development skills
  • Better performance than traditional websites

For Businesses:

  • Lower development and maintenance costs
  • Faster time to market
  • Better user engagement and retention
  • No app store approval process

PWA vs Traditional Web Apps vs Native Apps

2. How PWAs Work – The Technical Foundation

The Three Pillars of PWA

Every PWA is built on three core technologies:

1. HTTPS (Secure Connection)

PWAs require HTTPS because they deal with sensitive operations like caching and push notifications. This ensures data integrity and user privacy.

2. Web App Manifest

A JSON file that tells the browser how your app should behave when installed. It contains metadata like app name, icons, colors, and display preferences.

3. Service Worker

A JavaScript file that runs in the background, separate from your main application. It acts as a proxy between your app and the network, enabling offline functionality and caching.

The PWA Installation Process

Here’s what happens when a user installs your PWA:

  1. User visits your website via browser
  2. Browser checks PWA criteria: HTTPS, manifest, service worker, user engagement
  3. Browser shows install prompt (or user can manually install)
  4. User accepts installation
  5. Browser downloads manifest and icons
  6. App icon appears on home screen
  7. Future launches open in app-like mode

3. Web App Manifest Explained

What is a Web App Manifest?

The Web App Manifest is a JSON file that provides metadata about your web application. It tells the browser how your app should appear and behave when installed on a user’s device.

Manifest Structure Breakdown

Let’s examine each property and understand why it’s important:

{
  "name": "Tanyoe - Hotel Management System",
  "short_name": "Tanyoe",
  "description": "Complete hotel management solution",
  "start_url": "/",
  "scope": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#4B5563",
  "orientation": "portrait-primary",
  "lang": "en",
  "categories": ["business", "productivity"],
  "icons": [...]
}

Property Explanations:

  • name: Full app name shown during installation and in app stores
  • short_name: Short name for home screen icon (12 characters max recommended)
  • description: Brief description of what your app does
  • start_url: URL that opens when user launches the installed app
  • scope: Defines which URLs are considered part of your app
  • display: How the app appears when launched:
    • standalone: Looks like a native app (no browser UI)
    • fullscreen: Takes up entire screen
    • minimal-ui: Minimal browser UI
    • browser: Opens in regular browser tab
  • background_color: Color shown while app is loading
  • theme_color: Color of the browser’s address bar and status bar
  • orientation: Preferred screen orientation
  • categories: App store categories (helps with discoverability)
  • icons: Array of icon objects for different sizes and purposes

Icon Requirements Explained

PWAs need multiple icon sizes because different platforms and contexts use different sizes:

  • 144×144: Minimum required size for PWA installation
  • 192×192: Standard home screen icon size
  • 512×512: Used for splash screens and app stores

Icon purposes:

  • any: Standard icons used in most contexts
  • maskable: Icons designed to work with different shaped masks (Android adaptive icons)

Dynamic vs Static Manifests

Static Manifest: A fixed JSON file served from your public directory

public/manifest.json

Dynamic Manifest: Generated by your server based on user context

Route::get('/manifest.json', function () {
    $startUrl = auth()->check() ? '/dashboard' : '/login';
    return response()->json([...]);
});

Why use dynamic manifests?

  • Different start URLs based on user authentication
  • Personalized app names or themes
  • Different configurations for different user roles

4. Service Workers Deep Dive

What is a Service Worker?

A Service Worker is a JavaScript file that runs in the background, separate from your web page. Think of it as a proxy server sitting between your web app and the network.

Service Worker Lifecycle

Understanding the lifecycle helps you implement better caching strategies:

  1. Registration: Your main app registers the service worker
  2. Installation: Browser downloads and installs the service worker
  3. Activation: Service worker becomes active and can intercept network requests
  4. Fetch Handling: Intercepts all network requests from your app
  5. Update: When you deploy a new service worker, the process repeats

Key Service Worker Events

Install Event

Fired when service worker is first installed:

self.addEventListener('install', (event) => {
    console.log('[SW] Installing...');
    // This is where you cache your app shell (critical files)
    event.waitUntil(
        caches.open('app-cache-v1')
            .then(cache => cache.addAll([
                '/',
                '/css/app.css',
                '/js/app.js'
            ]))
    );
});

Why cache during install?

  • Ensures essential files are available offline
  • App shell (HTML, CSS, JS) loads instantly on repeat visits

Activate Event

Fired when service worker becomes active:

self.addEventListener('activate', (event) => {
    console.log('[SW] Activating...');
    // Clean up old caches here
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== 'app-cache-v1') {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

Why clean up during activate?

  • Removes outdated cached files
  • Prevents storage bloat
  • Ensures users get fresh content

Fetch Event

Fired for every network request your app makes:

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
            .then(response => {
                // Return cached version if available
                if (response) {
                    return response;
                }
                // Otherwise fetch from network
                return fetch(event.request);
            })
    );
});

Caching Strategies Explained

1. Cache First

Check cache first, fallback to network:

// Good for: Static assets (CSS, JS, images)
caches.match(request) || fetch(request)

2. Network First

Try network first, fallback to cache:

// Good for: Dynamic content, API calls
fetch(request).catch(() => caches.match(request))

3. Stale While Revalidate

Serve from cache, update cache in background:

// Good for: Content that changes occasionally
const cachedResponse = caches.match(request);
const fetchPromise = fetch(request).then(response => {
    cache.put(request, response.clone());
    return response;
});
return cachedResponse || fetchPromise;

Why Service Workers Matter for Laravel Apps

Traditional Laravel apps: Every page request hits the server Laravel apps with Service Workers: Cached resources load instantly, only dynamic data requires server requests

This dramatically improves perceived performance, especially on mobile devices with slow connections.

5. Authentication in PWA Context

The Authentication Challenge

Traditional web apps lose authentication state when the browser closes because sessions are tied to browser tabs. PWAs need to persist authentication across app launches.

Session vs Remember Token

Sessions: Temporary, tied to browser lifecycle Remember Tokens: Long-lived cookies that survive browser restarts.

// Without remember token (session only)
Auth::attempt($credentials, false); // Lost on browser close

// With remember token
Auth::attempt($credentials, true); // Persists for weeks

How Remember Tokens Work

  1. User logs in with “Remember Me” checked
  2. Laravel generates a random token and stores it in database
  3. Token is stored in long-lived cookie (default: 2 weeks)
  4. On subsequent visits, Laravel checks the cookie and automatically logs in the user

PWA Authentication Flow

User opens PWA → Service Worker loads app shell → 
JavaScript checks authentication → 
If authenticated: redirect to dashboard → 
If not authenticated: show login form

Why Traditional Session Handling Fails in PWA

Problem: PWAs feel like native apps, so users expect to stay logged in Solution: Always use remember tokens for PWA authentication

6. Step-by-Step Implementation

Now that you understand the concepts, let’s implement them:

Step 1: Configure Laravel for PWA Authentication

Update Session Configuration

// config/session.php
'lifetime' => 1440, // 24 hours instead of default 2 hours
'expire_on_close' => false, // Don't expire when browser closes
'secure' => true, // HTTPS required for PWA
'same_site' => 'lax', // Allow cross-site requests

Why these settings?

  • 24-hour lifetime: PWA users expect longer sessions
  • expire_on_close = false: Session survives browser restarts
  • secure = true: HTTPS requirement for PWA
  • same_site = lax: Allows PWA to work properly

Modify Authentication Controller

// app/Http/Controllers/Auth/AuthenticatedSessionController.php
public function store(Request $request): RedirectResponse
{
    $request->validate([
        'email' => ['required', 'string', 'email'],
        'password' => ['required', 'string'],
    ]);

    // Always remember for PWA, or respect checkbox
    $remember = $request->boolean('remember', true);

    if (!Auth::attempt($request->only('email', 'password'), $remember)) {
        throw ValidationException::withMessages([
            'email' => trans('auth.failed'),
        ]);
    }

    $request->session()->regenerate();
    return redirect()->intended('/dashboard');
}

Why default remember to true? PWA users expect app-like behavior where they stay logged in.

Step 2: Create Dynamic Web App Manifest

// routes/web.php
Route::get('/manifest.json', function () {
    // Dynamic start URL based on authentication
    $startUrl = auth()->check() ? '/dashboard' : '/login';
    
    return response()->json([
        'name' => 'Your App Name - Full Business Name',
        'short_name' => 'AppName', // 12 chars or less
        'description' => 'Brief description of what your app does',
        'start_url' => $startUrl, // Where to start when launched
        'scope' => '/', // Which URLs belong to this app
        'display' => 'standalone', // Hide browser UI
        'background_color' => '#ffffff', // Splash screen background
        'theme_color' => '#4B5563', // Status bar color
        'orientation' => 'portrait-primary', // Preferred orientation
        'lang' => 'en', // Primary language
        'categories' => ['business', 'productivity'], // App store categories
        'icons' => [
            [
                'src' => '/pwa-icon-144.png',
                'sizes' => '144x144',
                'type' => 'image/png',
                'purpose' => 'any'
            ],
            [
                'src' => '/pwa-icon-192.png',
                'sizes' => '192x192',
                'type' => 'image/png',
                'purpose' => 'any'
            ],
            [
                'src' => '/pwa-icon-512.png',
                'sizes' => '512x512',
                'type' => 'image/png',
                'purpose' => 'any'
            ]
        ]
    ])->header('Content-Type', 'application/manifest+json')
      ->header('Cache-Control', 'no-cache'); // Don't cache manifest
});

Why no-cache for manifest? The manifest might change based on user authentication state, so we don’t want browsers to cache old versions.

Step 3: Link Manifest in HTML Head

<!-- resources/views/app.blade.php -->
<head>
    <!-- Existing head content -->
    
    <!-- PWA Meta Tags -->
    <link rel="manifest" href="/manifest.json">
    <meta name="theme-color" content="#4B5563">
    
    <!-- Mobile optimizations -->
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="mobile-web-app-title" content="Your App">
    
    <!-- iOS specific -->
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="apple-mobile-web-app-title" content="Your App">
    <link rel="apple-touch-icon" href="/pwa-icon-192.png">
</head>

Why iOS-specific tags? Safari requires additional meta tags for proper PWA support on iOS devices.

Step 4: Create Service Worker

// routes/web.php
Route::get('/sw.js', function () {
    $swContent = <<<'JS'
const CACHE_NAME = 'your-app-v1';
const urlsToCache = [
    '/css/app.css',
    '/js/app.js',
    '/offline.html'
];

// Install: Cache essential files
self.addEventListener('install', (event) => {
    console.log('[SW] Installing...');
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then(cache => {
                console.log('[SW] Caching essential files');
                return cache.addAll(urlsToCache);
            })
            .catch(error => {
                console.log('[SW] Cache failed:', error);
            })
    );
    // Force immediate activation
    self.skipWaiting();
});

// Activate: Clean up old caches
self.addEventListener('activate', (event) => {
    console.log('[SW] Activating...');
    event.waitUntil(
        caches.keys().then(cacheNames => {
            return Promise.all(
                cacheNames.map(cacheName => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('[SW] Deleting old cache:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        }).then(() => {
            // Take control of all pages immediately
            return self.clients.claim();
        })
    );
});

// Fetch: Intercept network requests
self.addEventListener('fetch', (event) => {
    // Skip non-GET requests (forms, API calls)
    if (event.request.method !== 'GET') {
        return;
    }

    // Skip non-HTTP requests (Chrome extensions)
    if (!event.request.url.startsWith('http')) {
        return;
    }

    // Don't cache the homepage - let authentication redirects work
    if (event.request.url.endsWith('/') && !event.request.url.includes('?')) {
        return;
    }

    event.respondWith(
        caches.match(event.request)
            .then(response => {
                if (response) {
                    console.log('[SW] Serving from cache:', event.request.url);
                    return response;
                }

                // Not in cache, fetch from network
                return fetch(event.request)
                    .then(response => {
                        // Only cache successful responses
                        if (!response || response.status !== 200 || response.type !== 'basic') {
                            return response;
                        }

                        // Cache the response for future use
                        const responseToCache = response.clone();
                        caches.open(CACHE_NAME)
                            .then(cache => {
                                cache.put(event.request, responseToCache);
                            });

                        return response;
                    });
            })
            .catch(() => {
                // Network error, show offline page for navigation requests
                if (event.request.destination === 'document') {
                    return caches.match('/offline.html');
                }
            })
    );
});

// Handle messages from main app
self.addEventListener('message', (event) => {
    if (event.data?.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});
JS;

    return response($swContent)
        ->header('Content-Type', 'application/javascript')
        ->header('Cache-Control', 'no-cache, no-store, must-revalidate');
});

Key Service Worker Decisions Explained:

  1. Don’t cache homepage: Allows authentication redirects to work properly
  2. Skip non-GET requests: Forms and API calls should always go to server
  3. Cache successful responses: Only cache HTTP 200 responses to avoid caching errors
  4. Offline fallback: Show offline page when network fails

Step 5: Register Service Worker in Your App

// resources/js/app.tsx
if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js')
            .then(registration => {
                console.log('[SW] Registered successfully');
                
                // Handle service worker updates
                registration.addEventListener('updatefound', () => {
                    const newWorker = registration.installing;
                    newWorker?.addEventListener('statechange', () => {
                        if (newWorker.state === 'installed') {
                            // New service worker available, user can refresh
                            console.log('[SW] New version available');
                        }
                    });
                });
            })
            .catch(error => {
                console.log('[SW] Registration failed:', error);
            });
    });
}

Why register on ‘load’ event? Ensures your app’s main functionality loads first, then service worker registration happens in the background.

Step 6: Create App Icons

// routes/web.php - Dynamic icon generation
Route::get('/pwa-icon-{size}.png', function ($size) {
    if (!in_array($size, ['144', '192', '512'])) {
        abort(404);
    }
    
    $dimension = (int)$size;
    
    // Fallback for servers without GD extension
    if (!extension_loaded('gd')) {
        $pngData = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
        return response($pngData)->header('Content-Type', 'image/png');
    }
    
    // Create image with your brand colors
    $image = imagecreate($dimension, $dimension);
    $backgroundColor = imagecolorallocate($image, 75, 85, 99); // Your theme color
    $textColor = imagecolorallocate($image, 255, 255, 255); // White
    
    imagefill($image, 0, 0, $backgroundColor);
    
    // Add your app's first letter or logo
    $fontSize = $dimension > 300 ? 5 : ($dimension > 200 ? 4 : 3);
    $x = ($dimension - imagefontwidth($fontSize)) / 2;
    $y = ($dimension - imagefontheight($fontSize)) / 2;
    imagestring($image, $fontSize, $x, $y, 'A', $textColor); // Replace 'A' with your initial
    
    ob_start();
    imagepng($image);
    $imageData = ob_get_clean();
    imagedestroy($image);
    
    return response($imageData)
        ->header('Content-Type', 'image/png')
        ->header('Cache-Control', 'public, max-age=86400'); // Cache for 24 hours
});

Why generate icons dynamically?

  • No need to create and manage multiple icon files
  • Consistent branding across all icon sizes
  • Easy to update icons by changing code

Step 7: Configure Homepage Route

// routes/web.php
Route::get('/', function () {
    // Check authentication and redirect appropriately
    if (auth()->check()) {
        return redirect('/dashboard');
    }
    
    // Not authenticated, show welcome page
    return Inertia::render('Welcome')->withHeaders([
        'Cache-Control' => 'no-cache, no-store, must-revalidate',
        'Pragma' => 'no-cache',
        'Expires' => '0'
    ]);
})->name('home');

Why no-cache headers? Prevents browsers from caching the authentication decision, ensuring fresh auth checks on each visit.

Step 8: Implement Remember Me in Login Form

// Login form component
import { useState } from 'react';
import { Checkbox } from '@/Components/ui/checkbox';

export default function LoginForm() {
    const [data, setData] = useState({
        email: '',
        password: '',
        remember: true // Default to true for PWA
    });

    return (
        <form onSubmit={handleSubmit}>
            {/* Email and password fields */}
            
            <div className="flex items-center space-x-2">
                <Checkbox
                    id="remember"
                    name="remember"
                    checked={data.remember}
                    onCheckedChange={(checked) => setData('remember', Boolean(checked))}
                />
                <label htmlFor="remember">
                    Remember me for 2 weeks
                </label>
            </div>
            
            <button type="submit">Login</button>
        </form>
    );
}

Why default to true? PWA users expect to stay logged in like native apps. Making it opt-out rather than opt-in improves user experience.

7. Testing and Debugging

PWA Installation Testing

Desktop Chrome

  1. Open your app in Chrome
  2. Look for install icon in address bar (⊕ symbol)
  3. Click to install, or use Chrome menu > More Tools > Create Shortcut

Mobile Chrome

  1. Open your app in Chrome mobile
  2. Chrome menu (⋮) > Add to Home Screen
  3. Follow the installation prompts

PWA Criteria Check

Open Chrome DevTools > Application > Manifest:

  • All fields should be populated correctly
  • Icons should load without errors
  • Installability section should show no blocking issues

Authentication Persistence Testing

  1. Login Test: Login with remember me checked
  2. Close Test: Completely close browser/PWA
  3. Reopen Test: Open PWA again – should go directly to dashboard
  4. Logout Test: Logout should redirect to welcome/login page

Debug Routes for Development

// routes/web.php - Add these for development only
Route::get('/debug-auth', function () {
    return response()->json([
        'authenticated' => auth()->check(),
        'user_id' => auth()->id(),
        'remember_token' => auth()->user()?->remember_token,
        'session_id' => session()->getId(),
        'cookies' => array_keys(request()->cookies->all()),
    ]);
});

Route::get('/debug-pwa', function () {
    return response()->json([
        'manifest_url' => url('/manifest.json'),
        'service_worker_url' => url('/sw.js'),
        'icons_available' => [
            url('/pwa-icon-144.png'),
            url('/pwa-icon-192.png'),
            url('/pwa-icon-512.png'),
        ],
        'https_enabled' => request()->secure(),
    ]);
});

Browser Console Commands for Debugging

// Check service worker status
navigator.serviceWorker.getRegistrations().then(console.log);

// Check authentication
fetch('/debug-auth').then(r => r.json()).then(console.log);

// Check install prompt availability
window.addEventListener('beforeinstallprompt', (e) => {
    console.log('PWA installable!');
});

// Check cache contents
caches.keys().then(keys => {
    keys.forEach(key => {
        caches.open(key).then(cache => {
            cache.keys().then(requests => {
                console.log(`Cache ${key}:`, requests.map(r => r.url));
            });
        });
    });
});

8. Common Issues and Solutions

Issue: PWA Install Prompt Not Appearing

Symptoms: No install option in browser, “Add to Home Screen” not available

Possible Causes & Solutions:

  • Missing required icons: PWA needs at least 144×144 icon
// Check in console
   fetch('/pwa-icon-144.png').then(r => console.log('144px icon:', r.status));
  • HTTPS not working: PWAs require secure connection
   console.log('HTTPS enabled:', location.protocol === 'https:');
  • Service worker not registered: Check browser console for registration errors
   navigator.serviceWorker.getRegistrations().then(r => console.log('SW count:', r.length));
  • Insufficient user engagement: Chrome requires 30+ seconds of user interaction
    • Solution: Use Chrome menu > More Tools > Create Shortcut as alternative

Issue: User Gets Logged Out After Closing App

Symptoms: User has to login again every time they open the PWA

Root Cause: Remember token not working properly

Debugging Steps:

  • Check if remember token is being created:
   Visit /debug-auth - should show remember_token value
  • Verify login controller uses remember parameter:
   Auth::attempt($credentials, $request->boolean('remember', true));
  • Check database for remember_token column:
DESCRIBE users; -- Should show remember_token column
  • Verify session configuration:
// config/session.php
   'lifetime' => 1440, // 24 hours
   'expire_on_close' => false,

Issue: Infinite Redirect Loop

Symptoms: App keeps redirecting between pages endlessly

Common Causes:

  • Homepage cached by service worker: Service worker returns cached homepage that doesn’t check authentication
// Solution: Exclude homepage from caching
   if (event.request.url.endsWith('/')) {
       return; // Don't cache homepage
   }
  • Client-side and server-side auth checks conflict: Both trying to redirect simultaneously
// Solution: Handle redirects server-side only
   Route::get('/', function () {
       return auth()->check() ? redirect('/dashboard') : Inertia::render('Welcome');
   });

Issue: Service Worker Not Updating

Symptoms: Changes to your app don’t appear in installed PWA

Solutions:

  • Change cache name when updating app:
const CACHE_NAME = 'your-app-v2'; // Increment version
  • Clear application data during development:
    • DevTools > Application > Storage > Clear storage
  • Force service worker update:
navigator.serviceWorker.getRegistrations().then(registrations => {
       registrations.forEach(registration => registration.update());
   });

Issue: App Freezes When Closing

Symptoms: PWA becomes unresponsive when trying to close it

Causes & Solutions:

  • Heavy operations in beforeunload: Remove or optimize
// Bad
   window.addEventListener('beforeunload', () => {
       // Heavy database operations
   });
   
   // Good
   window.addEventListener('beforeunload', () => {
       console.log('App closing gracefully');
   });
  • Memory leaks from event listeners: Always clean up
useEffect(() => {
       const handler = () => { /* ... */ };
       window.addEventListener('resize', handler);
       
       return () => window.removeEventListener('resize', handler);
   }, []);

9. Best Practices

Security Best Practices

  • Never cache sensitive data in service worker:
// Don't cache API endpoints with sensitive data
   if (event.request.url.includes('/api/user') || 
       event.request.url.includes('/api/payments')) {
       return; // Always fetch from network
   }
  • Always validate authentication server-side:
// Don't rely on client-side authentication checks
   Route::middleware('auth')->group(function () {
       Route::get('/dashboard', /* ... */);
   });
  • Use HTTPS everywhere: PWAs require HTTPS, no exceptions
  • Clear sensitive data on logout:
public function destroy(Request $request) {
       Auth::logout();
       $request->session()->invalidate();
       $request->session()->regenerateToken();
       
       // Clear remember token
       return redirect('/')->withCookie(cookie()->forget('remember_web_...'));
   }

Performance Best Practices

  • Cache static assets aggressively:
// CSS, JS, images - cache first
   if (event.request.url.includes('/css/') || 
       event.request.url.includes('/js/') ||
       event.request.url.includes('/images/')) {
       // Cache first strategy
   }
  • Use network-first for dynamic content:
// API calls, user data - network first
   if (event.request.url.includes('/api/')) {
       // Network first strategy
   }
  • Preload critical resources:
// During service worker install
   cache.addAll([
       '/css/app.css',
       '/js/app.js',
       '/fonts/primary-font.woff2'
   ]);

Minimize service worker scope:

// Only cache what you need
   const urlsToCache = [
       '/css/app.css',
       '/js/app.js'
   ]; // Don't cache everything

Development Best Practices

  • Use version-based cache names:
// Change this when you deploy updates
   const CACHE_VERSION = '1.0.0';
   const CACHE_NAME = `your-app-${CACHE_VERSION}`;
  • Implement proper error handling:
self.addEventListener('fetch', (event) => {
       event.respondWith(
           caches.match(event.request)
               .then(response => response || fetch(event.request))
               .catch(error => {
                   console.error('Fetch failed:', error);
                   // Return fallback response
                   return caches.match('/offline.html');
               })
       );
   });
  • Test across different browsers and devices:
    • Chrome Desktop & Mobile
    • Firefox Desktop & Mobile
    • Safari Desktop & Mobile
    • Edge Desktop
  • Use Lighthouse for PWA auditing:
    • Open DevTools > Lighthouse
    • Run Progressive Web App audit
    • Address any failing criteria

Production Deployment Checklist

Pre-deployment Requirements

  • HTTPS certificate valid and working
  • All PWA icons generated (144px, 192px, 512px minimum)
  • Web app manifest accessible at /manifest.json
  • Service worker accessible at /sw.js
  • Authentication persistence tested
  • Offline functionality working
  • Install prompts appearing correctly
  • Cross-browser compatibility verified

Security Checklist

  • No sensitive data cached in service worker
  • Authentication always validated server-side
  • Remember tokens properly cleared on logout
  • HTTPS enforced across entire application
  • Content Security Policy (CSP) headers configured

Performance Checklist

  • Critical resources preloaded
  • Appropriate cache strategies implemented
  • Service worker update mechanism working
  • Offline fallbacks provide good user experience
  • App loads quickly on slow connections

Monitoring and Maintenance

Analytics to Track

  1. PWA Install Rate: Percentage of users who install your PWA
  2. Return Visitor Rate: How often users return to your PWA
  3. Session Duration: Time spent in PWA vs web version
  4. Offline Usage: How often users access app offline

Regular Maintenance Tasks

  1. Update service worker when deploying new versions
  2. Monitor error logs for service worker failures
  3. Test PWA functionality after each deployment
  4. Update icons and manifest as brand evolves
  5. Review and optimize caching strategies based on usage patterns

Service Worker Update Strategy

// Automatic updates (aggressive)
self.addEventListener('install', () => {
    self.skipWaiting(); // Force immediate activation
});

// Manual updates (user-controlled)
self.addEventListener('install', (event) => {
    // Wait for user approval before activating
    event.waitUntil(
        // Cache resources but don't activate immediately
        caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache))
    );
});

Conclusion

Building a PWA for Laravel + Inertia + React applications involves understanding several interconnected technologies:

  • Web App Manifest tells browsers how to install and display your app
  • Service Workers enable offline functionality and improved performance
  • Authentication persistence ensures users stay logged in across sessions
  • Proper caching strategies balance performance with fresh content

The key to success is understanding why each component works the way it does, not just copying code. This foundation will help you troubleshoot issues and adapt the implementation to your specific needs.

Remember that PWAs are not just about making your web app installable – they’re about creating native app-like experiences that users love to use. Focus on performance, offline capabilities, and seamless authentication to create truly engaging progressive web applications.

Next Steps

  1. Implement basic PWA following this guide
  2. Test thoroughly across different devices and browsers
  3. Gather user feedback on the PWA experience
  4. Iterate and improve based on real-world usage
  5. Consider advanced features like push notifications and background sync

Additional Resources

This guide provides the foundation you need to build robust PWAs with Laravel. The concepts and patterns outlined here will serve you well as you create more sophisticated progressive web applications.