# How GeoLibre PWA Service Worker Implements Offline Caching for Pyodide and PGlite

> Learn how the GeoLibre PWA service worker precaches and runtime-caches Pyodide and PGlite with a CacheFirst strategy for 30-day offline use. Enable full offline functionality after the initial load.

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

---

**The GeoLibre PWA service worker precaches the app shell and runtime-caches heavy engine binaries from jsDelivr using a `CacheFirst` strategy with 30-day expiration, enabling full offline use of Pyodide and PGlite after the first online load.**

The GeoLibre desktop application is a Progressive Web App (PWA) designed to function without network connectivity. Its offline capability relies on service worker caching strategies configured in [`apps/geolibre-desktop/vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts) that specifically handle cross-origin WebAssembly binaries. This article explains how Pyodide and PGlite engines are cached for offline use.

## Service Worker Architecture and Caching Strategy

GeoLibre uses the Vite PWA plugin to generate its service worker. The configuration separates caching into two distinct approaches: **precaching** for the app shell and **runtime caching** for heavy, lazily-loaded binaries.

According to the inline documentation in the Vite configuration:

> "The service worker precaches the app shell … and runtime-caches the heavy, lazily-fetched same-origin binaries (DuckDB-WASM + spatial extension, MapLibre feature plugins) … PGlite/PostGIS and the Pyodide runtime are fetched cross-origin …"

Source: [`apps/geolibre-desktop/vite.config.ts#L58-L66`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts#L58-L66)

This dual strategy ensures that the core application loads instantly from the cache while engine binaries are fetched on demand and stored for subsequent offline sessions.

## Runtime Caching Rules for CDN-Loaded Engines

The critical configuration for Pyodide and PGlite caching resides in the `runtimeCaching` array. A dedicated rule intercepts requests to `cdn.jsdelivr.net` that match specific path patterns for the Python and PostgreSQL engines.

### CacheFirst Handler Configuration

```ts
{
  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, // 30 days
    },
    cacheableResponse: { statuses: [0, 200] },
  },
}

```

Source: [`apps/geolibre-desktop/vite.config.ts#L94-L114`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts#L94-L114)

**Key implementation details:**

- **`CacheFirst` handler**: Returns cached responses immediately when available, falling back to network only on cache miss
- **Versioned URLs**: jsDelivr paths include exact versions (e.g., `/pyodide/v0.26.1/full/pyodide.wasm`), ensuring cache coherence across deployments
- **400-entry limit with 30-day retention**: Prevents storage exhaustion while allowing substantial engine libraries
- **Status code 0 handling**: Accommodates opaque responses from cross-origin requests

## Detecting Active Service Worker State

Before relying on cached engines, the application verifies that a service worker controls the page. The `hasActiveServiceWorker()` utility in [`offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/offline-tiles.ts) performs this check:

```ts
export function hasActiveServiceWorker(): boolean {
  return (
    typeof navigator !== "undefined" &&
    "serviceWorker" in navigator &&
    !!navigator.serviceWorker.controller
  );
}

```

Source: [`apps/geolibre-desktop/src/lib/offline-tiles.ts#L16-L22`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/offline-tiles.ts#L16-L22)

This boolean guard prevents the application from attempting offline operations when caching infrastructure is unavailable.

## Complete Offline Workflow

### 1. Service Worker Registration

The application manually registers the service worker in [`main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/main.tsx):

```tsx
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/service-worker.js");
}

```

### 2. First-Use Engine Fetching

When a user opens the Python console or executes a PostGIS query, the browser requests engine files from jsDelivr. The service worker intercepts these requests, applies the `CacheFirst` rule, and populates the `geolibre-cdn-engines` cache.

### 3. Subsequent Offline Operation

With `hasActiveServiceWorker()` returning `true`, the application bypasses network dependencies:

```tsx
if (hasActiveServiceWorker()) {
  // Cached Pyodide/PGlite binaries available
  await loadPyodideRuntime();   // Retrieves from geolibre-cdn-engines
} else {
  // Network-dependent fallback
}

```

### 4. Proactive Engine Warming

Applications can force early caching by triggering engine requests before they're needed:

```ts
async function warmPGlite() {
  // Dummy fetch triggers WASM and .data file caching
  await fetch(
    "https://cdn.jsdelivr.net/npm/@electric-sql/pglite@0.8.0/dist/pglite.wasm"
  );
  // Subsequent offline loads served from cache
}

```

## Supporting Files and Their Roles

| File | Purpose |
|------|---------|
| [`apps/geolibre-desktop/vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts) | Service worker configuration including `runtimeCaching` rules for Pyodide and PGlite |
| [`apps/geolibre-desktop/src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/offline-tiles.ts) | `hasActiveServiceWorker()` utility for cache availability checking |
| [`apps/geolibre-desktop/public/pyodide/pyodide-worker.js`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/public/pyodide/pyodide-worker.js) | Precached worker shim enabling Pyodide loading when CDN binaries are cached |
| [`apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts) | Provides `VITE_PYODIDE_INDEX_URL`; service worker caches binaries, not the index |

## Summary

- **Precaching + runtime caching**: GeoLibre uses Vite PWA to cache the app shell at build time and engine binaries at runtime
- **jsDelivr interception**: The `geolibre-cdn-engines` cache stores Pyodide, PGlite, CereusDB, and GDAL3.js from `cdn.jsdelivr.net`
- **CacheFirst with expiration**: 30-day retention and 400-entry limit balance offline capability with storage constraints
- **Versioned URLs prevent staleness**: Exact version strings in CDN paths ensure correct cache invalidation
- **Runtime detection**: `hasActiveServiceWorker()` guards offline operations against missing service worker support

## Frequently Asked Questions

### How does GeoLibre handle Pyodide updates without stale caches?

The jsDelivr URLs include exact version identifiers (e.g., `/pyodide/v0.26.1/`). When GeoLibre updates to a new Pyodide version, the URL changes, creating a new cache entry. The old version remains cached until naturally evicted by the `maxEntries` or `maxAgeSeconds` limits, but is never served for new requests.

### Can users force re-download of cached engines?

The service worker follows standard PWA update mechanisms. When GeoLibre redeploys with a new service worker version, the browser installs the updated worker, which can include revised `runtimeCaching` rules. Users can also clear site data through browser settings to manually purge the `geolibre-cdn-engines` cache.

### Why does the configuration cache status code 0 responses?

Cross-origin requests to jsDelivr without CORS headers return opaque responses with status code 0. The `cacheableResponse: { statuses: [0, 200] }` option ensures these valid but non-standard responses are stored in the cache, enabling offline functionality for engines loaded from third-party CDNs.

### What happens if a user exceeds the 400-entry cache limit?

The Cache API's LRU (least-recently-used) eviction policy removes oldest entries when `maxEntries` is exceeded. For GeoLibre, this means rarely-used engine versions are purged first, while actively-used Pyodide and PGlite binaries persist. The 30-day `maxAgeSeconds` provides additional protection against permanent cache growth.