# How GeoLibre Implements PWA Offline Support with Workbox

> Discover how GeoLibre achieves PWA offline support with Workbox. Learn about precaching the app shell and runtime-caching critical assets for seamless offline use.

- Repository: [Open Geospatial Solutions/GeoLibre](https://github.com/opengeos/GeoLibre)
- Tags: how-to-guide
- Published: 2026-07-31

---

**GeoLibre uses vite-plugin-pwa to generate a Workbox-based service worker that precaches the lightweight app shell while runtime-caching heavy assets and basemap tiles, enabling full offline functionality after the first online visit.**

GeoLibre is an open-source geospatial application built by opengeos that requires robust offline capabilities for field use. The project implements **PWA offline support** through a carefully architected Workbox configuration that balances initial load performance with comprehensive asset caching. This approach ensures the app remains functional without connectivity while minimizing the bandwidth required for first-time installation.

## Architecture Overview

The implementation follows a strict separation between **precaching** (the essential app shell) and **runtime caching** (large, lazily-loaded assets). This split prevents the initial download from becoming prohibitively large while guaranteeing that all necessary resources are available offline after first use. A custom stale-chunk reload mechanism coordinates with the service worker lifecycle to ensure updates do not unexpectedly discard user state.

## Service Worker Generation with vite-plugin-pwa

In [`apps/geolibre-desktop/vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts), the `pwaPlugin()` function returns a `VitePWA` configuration that integrates Workbox into the Vite build pipeline. The plugin is spread into the Vite plugins list using `...pwaPlugin()`.

The configuration explicitly disables automatic registration by setting `injectRegister: false`. This allows the application to coordinate service worker registration with custom stale-chunk reload logic in the entry point, preventing premature updates that could interrupt active user sessions.

### Precaching Configuration

The precaching strategy uses `globPatterns` to select HTML, JavaScript, CSS, and font files for immediate caching. However, the `globIgnores` array—defined as `HEAVY_PRECACHE_IGNORES`—explicitly excludes heavy binaries like MapLibre, DuckDB-WASM, and Cesium from the initial precache. This keeps the first-visit download small while allowing these chunks to be cached on first use via runtime strategies.

```ts
/* vite.config.ts – Workbox configuration */
function pwaPlugin(): Plugin[] {
  return VitePWA({
    disable: PWA_DISABLED,
    registerType: "autoUpdate",
    injectRegister: false,
    workbox: {
      globPatterns: ["**/*.{js,css,html,woff,woff2}"],
      globIgnores: HEAVY_PRECACHE_IGNORES,
      maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
      cleanupOutdatedCaches: true,
      clientsClaim: true,
      skipWaiting: true,
      navigateFallback: "index.html",
      // ... runtimeCaching array
    },
  }) as Plugin[];
}

```

## Runtime Caching Strategies

Workbox's `runtimeCaching` array defines three distinct caching strategies tailored to different asset types. Each uses the **CacheFirst** strategy to guarantee offline availability after the initial fetch.

### Same-Origin Assets

Assets served from `/assets/` (including lazy-loaded WebAssembly modules and fonts) are cached under the `geolibre-assets` cache name. The configuration limits this cache to 300 entries with a 30-day expiration, ensuring storage remains bounded while preserving recent assets.

```ts
{
  // Same-origin assets (MapLibre, DuckDB-WASM, etc.)
  urlPattern: ({ url, sameOrigin }) =>
    sameOrigin && url.pathname.includes("/assets/") && /\.(?:js|css|wasm|data|woff2?)$/.test(url.pathname),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-assets",
    expiration: { maxEntries: 300, maxAgeSeconds: 60 * 60 * 24 * 30 },
    cacheableResponse: { statuses: [0, 200] },
  },
}

```

### CDN-Loaded Engines

External engines loaded from `cdn.jsdelivr.net`—including Pyodide, PGlite, and CereusDB—are cached under `geolibre-cdn-engines`. Because these URLs contain exact version hashes, a redeploy automatically generates new URLs, eliminating stale content concerns while allowing 400 cached entries.

```ts
{
  // CDN-loaded engines (Pyodide, PGlite, CereusDB)
  urlPattern: ({ url }) =>
    url.hostname === "cdn.jsdelivr.net" &&
    (url.pathname.startsWith("/pyodide/") ||
      url.pathname.startsWith("/npm/@electric-sql/") ||
      url.pathname.startsWith("/npm/@cereusdb/") ||
      url.pathname.startsWith("/npm/gdal3.js")),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-cdn-engines",
    expiration: { maxEntries: 400, maxAgeSeconds: 60 * 60 * 24 * 30 },
    cacheableResponse: { statuses: [0, 200] },
  },
}

```

### Basemap Tiles

Basemap tiles from CORS-friendly hosts like OpenFreeMap and CARTO are cached under `geolibre-basemaps` with an 8000-entry limit. This enables offline map browsing after the user has visited an area while online.

```ts
{
  // Basemap tiles from OpenFreeMap or CARTO
  urlPattern: ({ url }) => /(?:^|\.)(?:openfreemap\.org|cartocdn\.com)$/.test(url.hostname),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-basemaps",
    expiration: { maxEntries: 8000, maxAgeSeconds: 60 * 60 * 24 * 30 },
    cacheableResponse: { statuses: [0, 200] },
  },
}

```

## Service Worker Registration and Lifecycle

The service worker registration is handled in [`apps/geolibre-desktop/src/main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/main.tsx) using the `registerSW` function imported from `virtual:pwa-register`.

### Registration in the Entry Point

Registration is performed conditionally for web builds only; in Tauri or embedded Jupyter environments, the function executes as a no-op. The configuration opts into `immediate: true` registration and provides a custom `onNeedReload` callback that intentionally performs no action, delegating update decisions to the stale-chunk reload logic.

```tsx
/* src/main.tsx – Service-worker registration */
import { registerSW } from "virtual:pwa-register";

registerSW({
  immediate: true,
  onNeedReload() {
    // No forced reload; stale-chunk logic handles it when needed
  },
  onRegisterError(error) {
    console.error("[GeoLibre] Service worker registration failed", error);
  },
});

```

### Stale-Chunk Reload Coordination

A separate module at [`apps/geolibre-desktop/src/lib/stale-chunk-reload.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/stale-chunk-reload.ts) monitors for 404 errors on lazily-loaded chunks. When a chunk is removed by a redeploy, this module triggers a controlled page reload rather than allowing Workbox's default immediate activation behavior. This coordination preserves user state by ensuring reloads occur only when absolutely necessary and at non-disruptive moments.

## Summary

- **vite-plugin-pwa** generates the Workbox service worker in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts), with `injectRegister: false` enabling custom registration logic.
- **Precaching** captures the app shell (HTML, JS, CSS) while excluding heavy binaries via `HEAVY_PRECACHE_IGNORES` to minimize initial download size.
- **Runtime caching** uses **CacheFirst** strategies for three asset categories: same-origin assets (`geolibre-assets`), CDN engines (`geolibre-cdn-engines`), and basemap tiles (`geolibre-basemaps`).
- **Controlled updates** are managed by stale-chunk reload logic in [`src/lib/stale-chunk-reload.ts`](https://github.com/opengeos/GeoLibre/blob/main/src/lib/stale-chunk-reload.ts), which overrides Workbox's default activation reload to prevent data loss.
- The implementation enables **full offline functionality** after the first online visit while maintaining lightweight initial loads.

## Frequently Asked Questions

### Why does GeoLibre disable automatic service worker registration?

The configuration sets `injectRegister: false` to coordinate service worker activation with the stale-chunk reload system. This prevents Workbox from automatically reloading the page when a new service worker activates, allowing the application to preserve user state and trigger reloads only when lazy-loaded chunks are actually missing.

### How does GeoLibre handle application updates without losing user data?

Instead of accepting Workbox's default immediate reload on update, GeoLibre implements a custom stale-chunk detection module at [`apps/geolibre-desktop/src/lib/stale-chunk-reload.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/stale-chunk-reload.ts). This module watches for 404 errors on dynamic imports and triggers a reload only when a requested chunk no longer exists, ensuring updates occur at non-disruptive moments.

### What heavy assets are excluded from precaching?

The `HEAVY_PRECACHE_IGNORES` constant excludes large binaries such as MapLibre, DuckDB-WASM, and Cesium from the initial precache. These assets are instead cached on first use via the runtime caching strategy, keeping the initial installation under the 4MB limit defined by `maximumFileSizeToCacheInBytes`.

### How does offline basemap tile caching work?

Basemap tiles from OpenFreeMap and CARTO domains are cached using a **CacheFirst** strategy with the cache name `geolibre-basemaps`. This stores up to 8,000 tiles for 30 days, enabling users to browse previously visited map areas without an internet connection.