# How to Implement Offline-First Data Workflows with Service Workers in GeoLibre

> Learn to implement offline-first data workflows with service workers in GeoLibre. Cache assets and map tiles for seamless offline access after one online visit.

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

---

**GeoLibre uses a Workbox-generated service worker via `vite-plugin-pwa` to cache static assets, WASM engines, and map tiles, enabling full offline functionality after the first online visit.**

GeoLibre is a Progressive Web App (PWA) designed for geospatial data workflows that must function without constant network connectivity. The repository implements offline-first architecture through a sophisticated service worker strategy that intercepts and caches heavy dependencies—ranging from DuckDB-WASM spatial extensions to Pyodide and PGlite engines—alongside map tile data. This approach ensures that once a user completes an initial online session, the entire application stack remains available locally, including the ability to download specific geographic areas for disconnected fieldwork.

## Core Architecture and Caching Strategy

The offline capability centers on a **Workbox service worker** generated by `vite-plugin-pwa`, configured in [`apps/geolibre-desktop/vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts). This setup creates distinct cache layers managed through CacheFirst strategies, ensuring that critical resources are stored locally after their first retrieval.

The primary runtime caches include:

- **`geolibre-basemaps`**: Stores map tiles from approved hosts (OpenFreeMap, CARTO) as they are fetched during normal map interaction or explicit offline area downloads.
- **`geolibre-cdn-engines`**: Caches large WASM engines (`Pyodide`, `PGlite/PostGIS`, `CereusDB`, [`gdal3.js`](https://github.com/opengeos/GeoLibre/blob/main/gdal3.js)) loaded from jsDelivr, eliminating redundant downloads of multi-megabyte files.
- **Precached App Shell**: The build process automatically hashes and precaches the JavaScript bundle, CSS, and DuckDB-WASM spatial extension.

The service worker applies **CacheFirst** logic only to known, CORS-friendly domains. External tile services outside the whitelist remain network-only to prevent unwanted data usage and cache pollution.

## Step-by-Step Offline Workflow Implementation

### 1. Initial Visit and Asset Seeding

When a user first loads GeoLibre in a browser, the service worker activates and populates caches transparently:

- Static assets install via Workbox precaching.
- DuckDB-WASM and feature plugins store in the runtime cache upon first import.
- Heavy engines download on-demand when first invoked (e.g., running a Python tool triggers Pyodide fetch and cache).

### 2. Explicit Tile Caching via Offline Area Downloads

The **Download Offline Area** tool provides granular control over map data storage. Located in [`apps/geolibre-desktop/src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/offline-tiles.ts), this utility enumerates every tile within a user-defined bounding box and zoom range, then triggers standard `fetch()` calls that the service worker intercepts.

The process follows this sequence:

1. `planOfflineZoom()` calculates optimal zoom levels based on current view state and user preferences.
2. `enumerateTiles()` generates the complete list of `{z}/{x}/{y}` coordinates covering the selected area.
3. The UI iterates through these coordinates, issuing `fetch()` requests for each tile URL.
4. The service worker matches these against the `geolibre-basemaps` CacheFirst route, storing responses for offline use.

### 3. Offline Session Operation

Once caching is complete, the application operates without network requests:

- Map tile requests serve directly from `geolibre-basemaps`.
- Python script execution loads Pyodide from the `geolibre-cdn-engines` cache.
- Spatial queries run against locally cached DuckDB-WASM instances.

## Code Examples for Implementation

### Planning Zoom Levels for Offline Downloads

Use `planOfflineZoom` from [`apps/geolibre-desktop/src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/offline-tiles.ts) to determine which zoom levels to cache based on current view parameters:

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

const zoomPlan = planOfflineZoom(
  viewZoom,      // Current fractional zoom level
  mapMaxZoom,    // Maximum zoom configured for the map
  includeExtra,  // Boolean: user requested extra detail
  extraLevels,   // Integer: additional zoom levels to include
  hardMax        // Integer: absolute UI-enforced zoom limit
);

// zoomPlan.baseZoom and zoomPlan.maxZoom define the range to cache
console.log(`Caching zoom levels ${zoomPlan.baseZoom} through ${zoomPlan.maxZoom}`);

```

### Enumerating and Caching Tile Coordinates

The `enumerateTiles` generator function yields every tile coordinate within a bounding box. Pair this with standard `fetch()` calls to populate the service worker cache:

```typescript
import { enumerateTiles, Bbox } from '@/lib/offline-tiles';

const bbox: Bbox = [-122.5, 37.7, -122.3, 37.8]; // [west, south, east, north]

for (const tile of enumerateTiles(bbox, zoomPlan.baseZoom, zoomPlan.maxZoom)) {
  const url = `https://openfreemap.org/tiles/${tile.z}/${tile.x}/${tile.y}.png`;
  
  // The service worker intercepts this fetch and caches the response
  fetch(url).catch(err => console.warn('Tile cache failed:', url, err));
}

```

### Registering Offline Basemap Styles

After downloading tiles, register the basemap configuration using utilities from [`apps/geolibre-desktop/src/components/layout/BasemapExtractPanel.tsx`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/components/layout/BasemapExtractPanel.tsx):

```typescript
import { registerOfflineBasemapStyle } from '@/components/layout/BasemapExtractPanel';

const styleUrl = await fetchStyleFromServer(); // MapLibre style JSON
const basemapId = registerOfflineBasemapStyle('field-survey-2024', styleUrl);

// The style is now cached and available offline via the service worker

```

### Conditional Service Worker Builds

The service worker is **only** generated for standalone web builds. Desktop (Tauri) and embedded Jupyter environments disable the PWA plugin to prevent scope conflicts:

```typescript
// apps/geolibre-desktop/vite.config.ts
const IS_TAURI_BUILD = !!process.env.TAURI_ENV_PLATFORM;
const IS_EMBED = process.env.GEOLIBRE_EMBED === '1';
const PWA_DISABLED = IS_TAURI_BUILD || IS_EMBED;

export default defineConfig({
  plugins: [
    !PWA_DISABLED && VitePWA({
      // Workbox configuration for runtime caching
      workbox: {
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/openfreemap\.org\/tiles\/.*/,
            handler: 'CacheFirst',
            options: { cacheName: 'geolibre-basemaps' }
          },
          {
            urlPattern: /^https:\/\/cdn\.jsdelivr\.net\/(pyodide|pglite)\/.*/,
            handler: 'CacheFirst',
            options: { cacheName: 'geolibre-cdn-engines' }
          }
        ]
      }
    })
  ].filter(Boolean),
});

```

## Managing Cached Data and Eviction

The **Basemap Extract Panel** ([`BasemapExtractPanel.tsx`](https://github.com/opengeos/GeoLibre/blob/main/BasemapExtractPanel.tsx)) provides UI controls for cache management. Users can trigger `evictOfflineBasemapStyle()` to remove specific basemaps, freeing storage space on device. The cache management store tracks metadata for each offline area, allowing renaming and selective deletion without corrupting the underlying service worker storage.

For operators requiring fully air-gapped deployments, environment variables enable self-hosting of engines:

- `VITE_PYODIDE_INDEX_URL`: Point to a same-origin Pyodide distribution.
- `GEOLIBRE_PGLITE_CDN=0`: Disable CDN fetching for PGlite, forcing local asset loading.

When engines load from the same origin as the application, they automatically fall under the standard CacheFirst rules alongside other static assets.

## Summary

- **Service Worker Generation**: GeoLibre uses `vite-plugin-pwa` in [`apps/geolibre-desktop/vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/vite.config.ts) to create a Workbox service worker that handles precaching and runtime strategies.
- **Cache-First Strategy**: Static assets, WASM engines, and basemap tiles store locally after first fetch, enabling offline operation.
- **Tile Enumeration**: The `planOfflineZoom` and `enumerateTiles` functions in [`offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/offline-tiles.ts) calculate and download specific geographic areas for disconnected use.
- **Build Targeting**: Service workers are automatically disabled for Tauri desktop builds and Jupyter embeddings via `IS_TAURI_BUILD` and `IS_EMBED` flags.
- **Cache Management**: The `BasemapExtractPanel` component exposes `registerOfflineBasemapStyle` and `evictOfflineBasemapStyle` for user-controlled storage management.

## Frequently Asked Questions

### How does GeoLibre handle offline caching for large WASM engines like Pyodide?

GeoLibre caches large engines using a dedicated `geolibre-cdn-engines` runtime cache defined in [`vite.config.ts`](https://github.com/opengeos/GeoLibre/blob/main/vite.config.ts). When a user first runs a Python tool, Pyodide fetches from jsDelivr; the service worker intercepts this response and stores it locally. Subsequent sessions load the engine from the cache without network traffic. Operators can also set `VITE_PYODIDE_INDEX_URL` to a self-hosted URL, causing the engine to cache as a same-origin asset.

### Can users download specific map regions for offline use?

Yes. The **Download Offline Area** dialog uses functions from [`apps/geolibre-desktop/src/lib/offline-tiles.ts`](https://github.com/opengeos/GeoLibre/blob/main/apps/geolibre-desktop/src/lib/offline-tiles.ts) to enumerate every tile URL within a user-selected bounding box and zoom range. The UI then issues standard `fetch()` requests for each tile; the service worker's `geolibre-basemaps` CacheFirst route captures these responses, making the selected region available offline without custom download protocols.

### Why is the service worker disabled in the desktop version of GeoLibre?

The service worker is disabled for Tauri builds because the desktop application bundles all assets directly into the binary, eliminating the need for network interception. Enabling a service worker in this environment could interfere with the host page's scope and create unnecessary complexity. The build system checks `process.env.TAURI_ENV_PLATFORM` and conditionally disables `vite-plugin-pwa` when `IS_TAURI_BUILD` is true.

### What happens if a user requests tiles from an uncached external service while offline?

The service worker only applies CacheFirst strategies to whitelisted domains (OpenFreeMap, CARTO, and configured CDN hosts). Requests to arbitrary external tile services follow default browser fetch behavior and will fail when offline. This design prevents silent cache pollution and ensures users understand which data sources are available in disconnected environments.