# How to Configure PWA Offline Caching with Workbox in GeoLibre

> Learn to configure PWA offline caching with Workbox in GeoLibre. VitePWA precaches your app shell and implements runtime caching for essential assets and basemap tiles.

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

---

**GeoLibre uses Vite's `VitePWA` plugin with Workbox underneath to generate a service worker that precaches the app shell and implements runtime caching for heavy assets, CDN engines, and basemap tiles.**

GeoLibre is an open-source geospatial desktop application built with React and Vite. Its Progressive Web App (PWA) capabilities rely on **Workbox** for reliable offline caching of critical resources. This guide breaks down the exact configuration found in the `opengeos/GeoLibre` repository so you can implement, customize, or extend the offline caching strategy.

## Understanding the Workbox Architecture in GeoLibre

The PWA configuration lives in [`apps/geolibre-desktop/vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts). Vite's `VitePWA` plugin invokes Workbox's `generateSW` at build time, producing a service worker that handles three distinct caching layers:

1. **Precaching** – Core app shell (HTML, JS, CSS, fonts)
2. **Runtime caching – Assets** – Large hashed chunks from `/assets/`
3. **Runtime caching – External resources** – CDN-hosted engines and basemap tiles

## Precaching the App Shell

The `globPatterns` setting tells Workbox which files to include in the precache manifest generated at build time.

```typescript
// vite.config.ts – workbox configuration
precaching: {
  globPatterns: ["**/*.{js,css,html,woff,woff2}"],
  globIgnores: HEAVY_PRECACHE_IGNORES,
  maximumFileSizeToCacheInBytes: 4 * 1024 * 1024, // 4 MB
}

```

- **`globPatterns`** – Matches all critical static assets in the build output
- **`globIgnores`** – Excludes heavy, lazily-loaded chunks (DuckDB-WASM, Pyodide, etc.) that would exceed the size limit
- **`maximumFileSizeToCacheInBytes`** – Prevents bloating the precache with files over 4 MB

Files matching these patterns are hashed and versioned; Workbox serves them cache-first and updates them when the build hash changes.

## Runtime Caching for Large Assets

Assets too heavy for precaching are cached on first fetch using the **CacheFirst** strategy.

```typescript
// vite.config.ts – runtimeCaching array
{
  urlPattern: ({ url }: { url: URL }) =>
    url.pathname.startsWith("/assets/") &&
    /\.(js|css|wasm|data|woff2?)$/.test(url.pathname),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-assets",
    expiration: { maxEntries: 200, maxAgeSeconds: 60 * 60 * 24 * 60 }, // 60 days
    cacheableResponse: { statuses: [0, 200] },
  },
}

```

This pattern catches:

- DuckDB-WASM binaries
- MapLibre GL JS bundles
- Font files and data chunks

The `geolibre-assets` cache stores up to 200 entries for 60 days with opaque-response support (`statuses: [0, 200]`).

## Runtime Caching for CDN Engines

GeoLibre loads computational engines from `cdn.jsdelivr.net`. These are cached aggressively so the app works offline after first load.

```typescript
// vite.config.ts – runtimeCaching for CDN engines
{
  urlPattern: ({ url }: { url: URL }) =>
    url.hostname === "cdn.jsdelivr.net" &&
    (
      url.pathname.includes("pyodide") ||
      url.pathname.includes("pglite") ||
      url.pathname.includes("cereus") ||
      url.pathname.includes("gdal")
    ),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-cdn-engines",
    expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 90 }, // 90 days
  },
}

```

The `geolibre-cdn-engines` cache holds Pyodide, PGlite, Cereus, and GDAL WebAssembly builds—totaling tens of megabytes but essential for offline geoprocessing.

## Runtime Caching for Basemap Tiles

Offline map viewing requires caching raster tiles from CORS-friendly tile servers.

```typescript
// vite.config.ts – runtimeCaching for basemaps
{
  urlPattern: ({ url }: { url: URL }) => {
    const hosts = ["tiles.openfreemap.org", "basemaps.cartocdn.com"];
    return hosts.includes(url.hostname) && /\/\d+\/\d+\/\d+\.p?\w+$/.test(url.pathname);
  },
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-basemaps",
    expiration: { maxEntries: 8000, maxAgeSeconds: 60 * 60 * 24 * 30 }, // 30 days
    cacheableResponse: { statuses: [0, 200] },
  },
}

```

The generous `maxEntries: 8000` supports the "Download Offline Area" feature implemented in [`src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/src/lib/offline-tiles.ts), which proactively fetches tile pyramids for user-defined regions.

## Service Worker Registration and Activation

In [`apps/geolibre-desktop/src/main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/main.tsx), the service worker is registered manually to integrate with stale-chunk reload logic:

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

registerSW({
  immediate: true,
  onNeedReload() {
    // No-op: prevents full-page reload on SW update
    // Stale chunks trigger separate reload via installStaleChunkReload
  },
  onRegisterError(error) {
    console.error("[GeoLibre] Service worker registration failed", error);
  },
});

```

Key behaviors:

- **`immediate: true`** – Registers the SW without waiting for page load completion
- **`onNeedReload` as no-op** – Keeps the current page alive; map state is preserved
- **`clientsClaim: true` and `skipWaiting: true`** in the generated SW – New SW activates immediately

The `installStaleChunkReload` utility (referenced in the same file) handles lazy-loaded chunks that may mismatch after deployment, requesting a controlled reload only when necessary.

## Customizing the Caching Configuration

### Add a New Tile Source

Append a runtime caching rule matching your tile server hostname and path pattern:

```typescript
// vite.config.ts – add to workbox.runtimeCaching
{
  urlPattern: ({ url }: { url: URL }) =>
    url.hostname === "mytileserver.example.com" &&
    url.pathname.match(/^\/tiles\/\d+\/\d+\/\d+\.png$/),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-custom-tiles",
    expiration: { maxEntries: 5000, maxAgeSeconds: 60 * 60 * 24 * 14 },
  },
}

```

### Increase Precache Size Limit

For larger app shells, raise the byte limit:

```typescript
// vite.config.ts
workbox: {
  maximumFileSizeToCacheInBytes: 6 * 1024 * 1024, // 6 MB
}

```

### Cache a New CDN Domain

Mirror the CDN engine pattern for additional hosts:

```typescript
// vite.config.ts – runtimeCaching entry
{
  urlPattern: ({ url }: { url: URL }) =>
    url.hostname === "unpkg.com" && url.pathname.includes("my-library"),
  handler: "CacheFirst",
  options: {
    cacheName: "geolibre-unpkg",
    expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 30 },
  },
}

```

## Workbox Options Reference

The complete Workbox configuration in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts) sets these additional options:

| Option | Setting | Purpose |
|--------|---------|---------|
| `clientsClaim` | `true` | New SW takes control of pages immediately |
| `skipWaiting` | `true` | Bypass waiting phase, activate instantly |
| `cleanupOutdatedCaches` | `true` | Remove old precached files on update |
| `sourcemap` | `true` | Debug service worker in DevTools |

These ensure users always receive the freshest cached content without manual intervention.

## Summary

- **Precaching** captures the app shell via `globPatterns` with a 4 MB size limit and explicit ignores for heavy chunks
- **Runtime caching** uses `CacheFirst` strategy for three categories: local assets (`geolibre-assets`), CDN engines (`geolibre-cdn-engines`), and basemap tiles (`geolibre-basemaps`)
- **Registration** in [`src/main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/src/main.tsx) opts for immediate activation with a no-op reload handler to preserve application state
- **Extension** requires adding URL patterns to the `runtimeCaching` array or adjusting `globPatterns` and size limits

## Frequently Asked Questions

### What is the maximum file size GeoLibre precaches by default?

GeoLibre sets `maximumFileSizeToCacheInBytes` to **4 MB** (4,194,304 bytes). Files exceeding this limit—such as DuckDB-WASM or Pyodide bundles—are excluded from precaching and handled via runtime caching instead. You can raise this value in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts) if your build produces larger critical assets.

### How does GeoLibre prevent page reloads when the service worker updates?

The `registerSW` call in [`src/main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/src/main.tsx) passes an empty `onNeedReload` callback. This disables the default VitePWA behavior of reloading the page when a new service worker activates. Instead, `clientsClaim: true` and `skipWaiting: true` let the new SW take control immediately. Stale lazy-loaded chunks are handled separately by `installStaleChunkReload`.

### Where is the "Download Offline Area" feature implemented?

The tile prefetching logic lives in [`apps/geolibre-desktop/src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/offline-tiles.ts). It calculates tile URLs for a geographic bounding box and zoom range, then fetches them into the `geolibre-basemaps` cache. This warms the runtime cache so tiles are available offline without waiting for normal map navigation.

### Can I use a different caching strategy than CacheFirst?

Yes—replace `handler: "CacheFirst"` with any Workbox strategy: `NetworkFirst`, `StaleWhileRevalidate`, `NetworkOnly`, or `CacheOnly`. For basemap tiles that update infrequently, `CacheFirst` is optimal. For live data overlays, `NetworkFirst` or `StaleWhileRevalidate` may be more appropriate.