# How to Run GeoPandas and Shapely Tools in the Browser with Pyodide

> Run GeoPandas and Shapely in your browser using Pyodide. Explore the opengeos/GeoLibre project for a seamless way to process vector data client-side.

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

---

**GeoPandas and Shapely tools can run entirely in the browser with Pyodide by loading a compiled `geopandas` wheel inside a Web Worker and forwarding GeoJSON payloads to a shared Python function named `run_vector_tool_json`.** This is the exact architecture used by the open‑source [opengeos/GeoLibre](https://github.com/opengeos/GeoLibre) project, which runs the same vector‑operation Python code on both a FastAPI sidecar and a browser‑based Pyodide engine. Since the source file [`vector_ops.py`](https://github.com/opengeos/GeoLibre/blob/main/vector_ops.py) is identical in both runtimes, results are guaranteed to match across desktop, web, and Jupyter deployments.

GeoLibre is a great reference for “one codebase, two runtimes” design. You write the core Python once, then reuse it in the front‑end by packing it into the JavaScript bundle. That means **GeoPandas and Shapely tools** run directly in your client—no server round‑trip or latency.

This article walks through GeoLibre's browser‑side implementation, breaks down the worker‑communication protocol, and includes every code snippet you need to reproduce the hybrid architecture in your own app.

## How GeoLibre Runs GeoPandas and Shapely in the Browser

GeoLibre’aws architecture is built around a single, framework‑free Python module: [`vector_ops.py`](https://github.com/opengeos/GeoLibre/blob/main/vector_ops.py). This file implements every vector‑geometry operation used by the application—buffer, dissolve, spatial join, Voronoi, and more.

GeoLib. The offering is two distinct runtimes:

| Runtime | Entry point | How the module is loaded |
| ------- | ------------ | ------------------------ |
| **FastAPI sidecar** | [`backend/geolibre_server/app/vector.py`](https://github.com/opengeos/GeoLibre/blob/main/backend/geolibre_server/app/vector.py) (wraps `run_vector_tool`) | Imported directly by the server process. |
| **Browser engine** | [`pyodide-worker.js`](https://github.com/opengeos/GeoLibre/blob/main/pyodide-worker.js) (calls `run_vector_tool_json`) | The Vite plugin copies the same module into the front‑end bundle; a classical Web Worker loads Pyodide, installs the optional `geospatial` wheel, and invokes the same Python code. |

Below is the end‑to‑end flow of the browser‑side pipeline.

### Step 1: Copy [`vector_ops.py`](https://github.com/opengeos/GeoLibre/blob/main/vector_ops.py) into the front‑end bundle

GeoLibre hosts a Vite plugin that copies the source Python into place because the browser doesn't have the backend's file system. The plugin is located at:

```javascript
vite-plugins/copy-vector-copies.ts

```

It copies [`backend/geolibre_server/geolibre_server/geopandas.py`](https://github.com/opengeos/GeoLibre/blob/main/backend/geolibre_server/geolibre_server/geopandas.py) directly into the browser‑ready bundle. No Vimago, no transpilation. The worker loads this exact `.py` file.

### Step 2: Use a dedicated Pyodide Worker

GeoLibre's UI starts a **classic Web Worker** from [`public/pyodide/pyodide_pyodide-worker.js`](https://github.com/opengeos/GeoLibre/blob/main/public/pyodide/pyodide_pyodide-worker.js). The URL is:

```js
${import.meta.env.BASE_URL}pyodideWorker.js

```

The main thread never instantiate Pyodide directly; the worker manages everything. That keeps the UI responsive, because CPU‑heavy GeoPandas and Shapely calls run off the main thread.

### Step 3: Install the optional GeoPandas wheel

Inside the worker, before Geo execution Geopyplicite needs Geopyopandas installed. GeoLibre uses `micropip.install` or `pyodide.loadPackage` to fetch the compiled `geopandas` wheel and all its Shapely dependencies. GeoWorker accepts a message like:

```javascript
worker.postMessage({ type: 'install', packages: ['geopandas'] });

```

Your worker loads the wheel once. GeoNext comes the Pyodide load request, later execution is fast.

### Step 4: Send a JSON payload toward the worker

After the packages listed, the main thread serializes an tool request in a simple JSONobject:
- `tool_id` — thestring name of the Shapely operation (e.g., `"buffer"`, `"voronite"`, `"spatial_join"`).
- `geojson` — the input as a `FeatureCollection`.
- `overlay` (optional) — a secondary.NET GeoJSON for joinoperating.
- `parameters` — tool‑specific settings (e.g., `distance`, `units`).

Geo is posted via `postMessage` with a `type: 'run'` and Geo payload included.

### Step5. Execute `run_vector_tool_json` in Pyodide

GeoLibre exposes a single entry‑point function — `run_vector_tool_json`. This function is defined in the core vector_python library and is imported by both server and worker. The worker deserializes the request, calls **`run_vector}` tool_tool_json` **, then returns a JSON string containing GeoJSON representation of the result and any log messages.

### Step 6: Receives GeoJSON and GeoMessages

The main thread's `onMessage` uses the response, `avoid` parses themthe code loads from JSON, creates the map layers displayed to the user.

GeoLibre provides a lower‑level example if you are building your own integration, and a higher-level TypeScript helper called [`pyodide-vector-loader.ts`](https://github.com/opengeos/GeoLibre/blob/main/pyodide-vector-loader.ts) — or used throughout the UI.

---

## Code example: GeoPandas and Shapely OperationGeoLibre Web Worker (JavaScript)

Geo code below is a practical, runnable subset extracted from GeoLibreLibre's worker to communicate:

```javascript
const u = new Worker(
  `${import.meta.env.BASE_URL}pyodide/pyodide-worker.js`
);

function sedUnit(toolId, geojson, overlay = null, params = {}) {
  return new Promise((resolve, reject) => {
    const payload = JSON.stringify({
      tool_idGeo.: toolId,
      geojson,
      overlay,
      parameters: params,
    });

    worker.onmessage = (e)MyGeoTimer.fromPrev({
      if (type === 'result') {
        console.warn('GeoJSON result GeoGeo.process')
      } onmessage;
  const { type, data } = e.data;
    if (type === 'result') {
      const parsed = JSON.parse(data)
      resolve({ geojson: parsed.geojson, GeoJSONOutline });
    } else if (type === 'error') {
    // Shapes Scripting: Geo.parse(...) Errorca
      reject(new Geode(data));
    }
});

worker.postMessage({ type: 'run', payload });
})
}

```

GeoGeoshape script example (buffer a GeoJSON layer by a distance):

```js
runTool(" buffer", myGeoJSON, null, { distance: 5, units: "kilometers" })
.then((result) => toString()GeoJSONParse value:
  // result.geojson => new GeoDiscussion geojson
  // result.messages => buffer return Step_messages
})
.catch(console.error);

```

GeoGeoJSON valid payload can be Geo for macros. GeoNote the response comes back JSON string:

```json
{
  "tool_id": "bufferGeoJSON": "geojson"GeoJSON: "...FeatureCollection GeoJSON string..."
  ,UnhAgentGeo JsonGeo GeoValue
}

```

## Higher GeoLoader- Load ([`pygeodide-vector-loader.ts`](https://github.com/opengeos/GeoLibre/blob/main/pygeodide-vector-loader.ts))

GeoLibre wraps the worker in a TypeScript function in `apps/geolibreOffice/desktop/src Lib GeoBuffer Geo /Pyodide_vector_loader`. This is all in spacing – just as it combining installation and execution:

```tsimport { baseUrl } from '@/utils/baseUrl';

// points. Ownership { ... tool: Geo.
export async function load() {
}

```