# How GeoLibre Implements PWA Offline Support with Workbox Service Workers

> Discover how GeoLibre achieves PWA offline support using Workbox service workers. Learn about its unique architecture for seamless offline map functionality after your first visit.

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

---

**GeoLibre uses a three-part architecture combining the Vite-PWA plugin's Workbox integration, manual service worker registration, and warm-up fetch logic to enable complete offline map functionality after the first online visit.**

The opengeos/GeoLibre repository demonstrates a production-ready approach to **PWA offline support with Workbox service workers** in a complex geospatial application. This article breaks down the exact implementation patterns, configuration decisions, and code paths that make offline vector and raster mapping possible.

## Overview: Three Cooperating Components

GeoLibre's offline capability depends on three systems working together:

1. **Build-time Workbox configuration** — defines precache and runtime caching rules
2. **Manual service worker registration** — gives the app control over SW lifecycle
3. **Runtime warm-up fetches** — populates caches through the SW rather than bypassing it

This design keeps initial page loads fast while ensuring heavy geospatial engines and basemap tiles become available for subsequent offline use.

## Vite-PWA Plugin and Workbox Configuration

The foundation sits in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts), where the `pwaPlugin()` function configures Workbox behavior (lines 81–111). Key decisions in this configuration include:

- `injectRegister: false` — disables automatic SW injection so the app can register manually
- Precache patterns via `globPatterns` — captures JS, CSS, HTML, and font assets needed for the app shell
- Deliberate exclusions via `globIgnores` — leaves out large lazy-loaded chunks (MapLibre, DuckDB-WASM, Cesium) to reduce first-visit bundle size

The `runtimeCaching` section establishes **Cache-First strategies** for three asset categories:

| Asset Type | Strategy | Example Sources |
|------------|----------|-----------------|
| Same-origin hashed assets | CacheFirst | Build chunks with content hashes |
| CDN-loaded geospatial engines | CacheFirst | Pyodide, PGlite, CereusDB via jsdelivr |
| Basemap tiles | CacheFirst | `geolibre-basemaps` and other allowed tile hosts |

Version-pinned CDN URLs ensure cache correctness: when `PGLITE_CDN_URL` or similar constants update with new package versions, the URL changes and Workbox fetches fresh rather than serving stale engines.

## Manual Service Worker Registration

With `injectRegister: false`, GeoLibre takes control of **when and how the service worker activates**. The registration code typically lives in [`src/main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/src/main.tsx), coordinated with the stale-chunk-reload logic from [`src/lib/stale-chunk-reload.ts`](https://github.com/opengeos/GeoLibre/blob/main/src/lib/stale-chunk-reload.ts):

```typescript
// src/main.tsx — simplified manual registration
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then(() => console.info('[GeoLibre] SW registered'))
    .catch(err => console.error('[GeoLibre] SW registration failed', err));
}

```

This manual approach lets the application:
- Share lifecycle state with the stale-chunk reload coordinator
- Control timing relative to other initialization steps
- Handle registration failures with custom UI feedback

## Warm-Up Logic: Driving the Cache Through the SW

The [`src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/src/lib/offline-tiles.ts) module implements the **user-facing offline region download feature**. Rather than writing directly to Cache Storage—which would bypass Workbox's routing and metadata—the module uses standard `fetch()` requests that flow through the active service worker.

### URL Collection and Deduplication

The `collectOfflineUrls()` function (lines 404–417) gathers every URL needed for a selected region:

```typescript
// Resolving URLs for an offline download
const { urls, tileUrls } = await collectOfflineUrls(
  map,
  bbox,      // [-122.5, 37.7, -122.3, 37.8]
  minZoom,   // 10
  maxZoom    // 14
);

```

Supporting functions handle source-specific logic:
- `resolveTileSources()` — fetches TileJSON when sources provide only a `url` field
- `clampZoomRange()` — prevents over-zoom requests that would 404
- `collectTileUrls()` and `collectStyleAssetUrls()` — enumerate tiles, sprites, and glyphs

All URLs are deduplicated so the preview size matches actual download requirements.

### Bounded Concurrent Fetching

The `warmUrls()` function (lines 443–514) executes the download with controlled concurrency:

```typescript
import { warmUrls } from '@/lib/offline-tiles';

const progress = await warmUrls(urls, {
  concurrency: 8,        // parallel fetches
  timeoutMs: 30_000,     // per-request timeout
  onProgress: p => console.log(`Progress: ${p.done}/${p.total}`, p)
});

// progress.failedUrls contains any URLs that need retry

```

Each fetch passes through the active SW, matching Workbox's `CacheFirst` routes and populating the corresponding cache entries. The bounded concurrency prevents overwhelming the browser or tile servers.

### Service Worker Presence Check

Before starting downloads, the UI verifies SW readiness using `hasActiveServiceWorker()` (lines 16–24):

```typescript
import { hasActiveServiceWorker } from '@/lib/offline-tiles';

if (!hasActiveServiceWorker()) {
  alert('Offline download requires the service worker to be active. Reload the page and try again.');
}

```

This guard returns `true` only when `navigator.serviceWorker.controller` exists, ensuring cache population won't silently fail.

## Step-by-Step Offline Flow

| Step | Action | Implementation Location |
|------|--------|------------------------|
| 1 | Vite build runs `pwaPlugin()`, Workbox generates [`sw.js`](https://github.com/opengeos/GeoLibre/blob/main/sw.js) with precache manifest and runtime routes | [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts) lines 81–111 |
| 2 | App boots and manually registers [`/sw.js`](https://github.com/opengeos/GeoLibre/blob/main//sw.js) | [`src/main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/src/main.tsx) |
| 3 | User selects offline region; `collectOfflineUrls()` resolves all tile, sprite, and glyph URLs | [`offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/offline-tiles.ts) lines 404–417 |
| 4 | `warmUrls()` fetches URLs; SW intercepts each request and applies `CacheFirst` caching | [`offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/offline-tiles.ts) lines 443–514 |
| 5 | Subsequent launches (including offline) retrieve assets from SW caches | Generated [`sw.js`](https://github.com/opengeos/GeoLibre/blob/main/sw.js) |
| 6 | Failed tiles reported in `progress.failedUrls` for UI retry handling | `warmUrls()` return value |

## Design Decisions and Trade-offs

**Cache-First for runtime assets** prioritizes offline availability over freshness. For geospatial engines with version-pinned URLs, this is safe—the URL content never changes. For basemap tiles, users accept potentially stale tiles in exchange for offline functionality.

**Excluding heavy chunks from precache** keeps Time-to-Interactive low on first visit. MapLibre, DuckDB-WASM, and Cesium load on-demand and cache thereafter, making them available offline without blocking initial render.

**Graceful degradation in `warmUrls()`** counts failures rather than aborting. A partially warmed region remains usable, and the UI can offer retry functionality for specific failed tiles.

## Summary

- **Workbox configuration** in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts) separates app shell precaching from lazy runtime caching of heavy engines and tiles
- **Manual registration** in [`main.tsx`](https://github.com/opengeos/GeoLibre/blob/main/main.tsx) coordinates the service worker lifecycle with app initialization and stale-chunk handling
- **Warm-up fetches** through [`offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/offline-tiles.ts) populate caches by flowing requests through the SW, respecting Workbox routing rules
- **Bounded concurrency and timeout handling** prevent overwhelming network resources during region downloads
- **Version-pinned CDN URLs** eliminate cache invalidation problems for external geospatial engines

## Frequently Asked Questions

### What prevents stale geospatial engines from being served offline?

GeoLibre uses **version-pinned CDN URLs** with exact package versions embedded in the path (e.g., `PGLITE_CDN_URL`). When dependencies upgrade, the URL changes and Workbox treats it as a new resource, fetching the updated version rather than serving a stale cache entry.

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

The `injectRegister: false` setting allows the application to **control registration timing** and share lifecycle state with the stale-chunk-reload logic in [`src/lib/stale-chunk-reload.ts`](https://github.com/opengeos/GeoLibre/blob/main/src/lib/stale-chunk-reload.ts). This coordination ensures predictable behavior when lazily-loaded chunks 404 after a new deployment.

### How does the offline region download handle partial failures?

The `warmUrls()` function in [`offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/offline-tiles.ts) **counts failed fetches without aborting** the entire operation. It returns a `failedUrls` array in the progress object, enabling the UI to show which tiles failed and offer a retry button for those specific URLs.

### Can the service worker cache tiles from any basemap provider?

No—Workbox `runtimeCaching` routes in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts) explicitly whitelist allowed tile hosts. The `geolibre-basemaps` source and other configured origins match the CacheFirst strategy; requests to unconfigured hosts bypass the SW and fail when offline.