How to Run Python GeoPandas and Shapely in the Browser with Pyodide: No Server Required

GeoLibre enables full GeoPandas and Shapely vector processing directly in the browser using Pyodide, eliminating any server dependency while maintaining identical results to the native FastAPI side-car.

Running Python geospatial libraries in the browser has historically required a back-end server. The opengeos/GeoLibre project breaks this constraint by embedding Pyodide—a WebAssembly-based Python runtime—directly into its web build. This article explains the complete architecture, from shared Python modules to Web Worker execution, so you can implement the same pattern in your own applications.

Shared Python Code: One Source of Truth

The foundation of GeoLibre's browser-side processing is vector_ops.py, a pure-Python module containing all core geometry functions. Located at backend/geolibre_server/geolibre_server/vector_ops.py, this file serves both the native FastAPI side-car and the Pyodide browser engine.


# backend/geolibre_server/geolibre_server/vector_ops.py

# Core functions for buffer, intersection, union, dissolve, etc.

# Used identically by server and browser builds

By maintaining a single implementation, GeoLibre guarantees that vector operations produce identical results regardless of execution environment.

Build-Time Integration: Vite Copy Plugin

To make the Python module available in the browser, a custom Vite plugin copies vector_ops.py into the frontend bundle during build:

// apps/geolibre-desktop/vite-plugins/copy-vector-ops.ts
// Copies vector_ops.py to src/lib/pyodide/vector_ops.generated.py

This transformation happens automatically, ensuring the browser always has the latest shared logic without manual file synchronization.

Pyodide Runtime Configuration

The Pyodide runtime URL is configurable via environment variable, defaulting to the public jsDelivr CDN:

// apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts
const PYODIDE_VERSION = "0.25.0"; // or specified version
const indexURL = import.meta.env.VITE_PYODIDE_INDEX_URL 
  ?? `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`;

Key configuration points:

  • First load cost: The Pyodide WASM payload downloads once (~tens of MB)
  • Subsequent operations: Reuse the warmed-up interpreter for near-instant execution
  • Self-hosting option: Point VITE_PYODIDE_INDEX_URL to your own mirror for air-gapped environments

Web Worker Execution Architecture

A dedicated Web Worker at public/pyodide/pyodide-worker.js manages the Pyodide lifecycle:

// apps/geolibre-desktop/src/lib/pyodide/pyodide-worker.js
// Loads Pyodide, installs geopandas, imports vector_ops.generated.py
// Executes tools over JSON-serialized message passing

The worker pattern prevents UI blocking during heavy geometry operations. It loads Pyodide from the configured CDN, installs the geopandas package (which includes Shapely), then imports the copied vector_ops.generated.py module.

UI Integration: Automatic Engine Selection

The VectorToolsDialog component automatically selects the appropriate engine based on build configuration:

// apps/geolibre-desktop/src/components/processing/VectorToolsDialog.tsx
// IS_MAS_BUILD true → automatically uses Pyodide engine
// Otherwise → connects to FastAPI side-car

When Pyodide is active, the pyodide-vector-loader handles parameter marshaling, progress streaming, and result conversion:

// apps/geolibre-desktop/src/lib/pyodide/pyodide-vector-loader.ts
interface VectorToolParams {
  tool: string;
  layerId: string;
  params: Record<string, any>;
  onProgress?: (percent: number) => void;
}

export async function runVectorToolInPyodide(
  params: VectorToolParams
): Promise<GeoJSON.FeatureCollection> {
  // Forwards to worker, awaits result, streams progress
}

Complete Usage Examples

From the Application UI

import { runVectorToolInPyodide } from "./pyodide/pyodide-vector-loader";

async function bufferLayer(layerId: string, distance: number) {
  const result = await runVectorToolInPyodide({
    tool: "buffer",
    layerId,
    params: { distance, resolution: 16 },
    onProgress: (p) => console.log(`Progress: ${p}%`),
  });
  // result is GeoJSON FeatureCollection ready for map display
  addGeoJsonLayer(result);
}

Direct Pyodide in Custom Console

const pyodide = await loadPyodide({
  indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/",
});

await pyodide.loadPackage(["geopandas", "shapely"]);

const geojson = pyodide.runPython(`
import geopandas as gpd
from shapely.geometry import Point

# Create sample data

gdf = gpd.GeoDataFrame(
    {"name": ["A", "B"]},
    geometry=[Point(0, 0), Point(1, 1)],
    crs="EPSG:4326"
)

# Reproject and buffer

gdf_utm = gdf.to_crs("EPSG:32633")
buffered = gdf_utm.buffer(1000)  # 1km buffer in meters

# Return as GeoJSON

gdf_utm.set_geometry(buffered).to_json()
`);

const featureCollection = JSON.parse(geojson);

File Reference Table

Component Path Purpose
Shared geometry logic backend/geolibre_server/geolibre_server/vector_ops.py Single source of truth for all vector operations
Build-time copy plugin apps/geolibre-desktop/vite-plugins/copy-vector-ops.ts Copies Python module into frontend bundle
Runtime configuration apps/geolibre-desktop/src/lib/pyodide/pyodide-config.ts CDN URL and version management
Web Worker apps/geolibre-desktop/public/pyodide/pyodide-worker.js Pyodide lifecycle and package installation
UI bridge apps/geolibre-desktop/src/lib/pyodide/pyodide-vector-loader.ts Parameter passing and progress streaming
Engine selection apps/geolibre-desktop/src/components/processing/VectorToolsDialog.tsx Automatic Pyodide/side-car routing

Performance Considerations

  • Initial load: 10-30 MB WASM download depending on Pyodide version and packages
  • Package caching: geopandas and dependencies download once, cached by browser
  • Memory limits: Constrained by browser tab (typically 2-4 GB for modern browsers)
  • Suitable workloads: Vector operations on datasets up to ~100k features depending on complexity

For larger datasets, the FastAPI side-car remains available as a fallback option.

Summary

  • Single Python module: vector_ops.py ensures identical results across server and browser
  • Build-time copying: Vite plugin integrates Python code into the frontend bundle
  • Configurable runtime: Environment variable controls Pyodide CDN or mirror URL
  • Worker isolation: Web Worker prevents UI blocking during geometry operations
  • Automatic fallback: UI components transparently select browser or server engine
  • Zero server dependency: Full GeoPandas/Shapely capabilities available offline after initial load

Frequently Asked Questions

What file size should users expect when loading Pyodide for the first time?

The initial download is approximately 10-30 MB depending on the Pyodide version and which packages are loaded. GeoLibre loads geopandas which includes Shapely and its dependencies. This payload caches in the browser, making subsequent visits nearly instant.

How does GeoLibre guarantee the same results between browser and server processing?

Both environments execute the identical Python code from backend/geolibre_server/geolibre_server/vector_ops.py. A Vite plugin copies this file into the frontend at build time, ensuring the browser and FastAPI side-car always run the same implementation.

Can Pyodide handle the same dataset sizes as a native Python environment?

Browser memory constraints limit Pyodide to smaller datasets than a dedicated server—typically tens to hundreds of thousands of features depending on geometry complexity. GeoLibre provides automatic fallback to the FastAPI side-car when server processing is available.

Is internet connectivity required after the initial Pyodide load?

Only if self-hosting is not configured. By default, GeoLibde uses the jsDelivr CDN. Setting VITE_PYODIDE_INDEX_URL to a local mirror enables fully offline operation after the first download.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →