How GeoLibre Performs Client-Side Vector Analysis with Turf.js and DuckDB-WASM Spatial

GeoLibre performs client-side vector analysis using a dual-backend system in the @geolibre/processing workspace: Turf.js handles lightweight geometry operations directly on GeoJSON, while DuckDB-WASM Spatial scales to large datasets and complex spatial SQL queries entirely in the browser.

GeoLibre's approach to client-side vector analysis combines two powerful JavaScript engines to keep geospatial processing fast, private, and offline-capable. The processing system lives in the @geolibre/processing workspace and operates on layers stored in the application's Zustand store. Depending on data volume and operation complexity, it automatically selects Turf.js for quick geometry tasks or DuckDB-WASM Spatial for heavy-duty spatial SQL.

Turf.js for Lightweight Geometry Operations

For most geometry-centric operations—buffer, centroid, union, simplify, and similar—GeoLibre calls Turf.js directly on the selected GeoJSON feature collection.

The pattern follows three steps in packages/processing/src/vector-tools.ts (lines 1-20 and 84-95):

  1. Fetch the input layer from context
  2. Execute the Turf helper with user parameters
  3. Filter null geometries and add results as a new layer
import buffer from "@turf/buffer";
import { featureCollection } from "@turf/helpers";

export const bufferTool: ProcessingAlgorithm = {
  // ...
  run: (ctx) => {
    const fc = requireFeatures(ctx);               // get the GeoJSON layer
    const distance = numberParam(ctx, "distance", 1);
    const units = (ctx.parameters.units as string) || "kilometers";

    // Turf does the heavy lifting on the client thread
    const buffered = buffer(fc, distance, { units });
    const features = (buffered?.features ?? []).filter(f => Boolean(f?.geometry));

    ctx.log(`Buffered ${features.length} feature(s) by ${distance} ${units}`);
    ctx.addResultLayer?.("Buffer", featureCollection(features));
  },
};

All Turf-based tools in vector-tools.ts follow this consistent pattern. Because these operations run entirely in JavaScript on the main thread, they complete instantly for modest datasets—typically a few thousand features or fewer.

DuckDB-WASM Spatial for Large Datasets and Complex Queries

When data volume grows or operations require spatial joins, topology analysis, or SQL-style aggregations, GeoLibre switches to DuckDB-WASM with the Spatial extension. This keeps processing client-side while leveraging a columnar SQL engine.

How DuckDB-WASM Integration Works

topology-tools.ts (line 140) and duckdb-processing.ts implement a four-step pipeline:

  1. Ensure the spatial extension is loaded — await duckdb.ensureExtensions(["spatial"])
  2. Register GeoJSON as a DuckDB table — duckdb.registerGeoJson(tagFeatureIndexes(fc))
  3. Execute spatial SQL queries — Standard PostGIS-compatible functions like ST_Union, ST_Intersects
  4. Convert results back to GeoJSON — Rows become a FeatureCollection for the map
const duckdb = requireDuckDb(ctx);
await duckdb.ensureExtensions(["spatial"]);
const registered = await duckdb.registerGeoJson(tagFeatureIndexes(fc));
const rows = await duckdb.query(`
  SELECT ST_Union(geometry) AS geometry
  FROM ${registered.table}
`);

Because DuckDB runs as WebAssembly in a separate thread, the pipeline stays fully client-side—no network requests, no server round-trips. The WASM runtime handles heavy computation without blocking the UI.

How the Dual-Backend System Chooses Engines

Each processing algorithm declares supportsSidecar: true. At runtime, runner.ts orchestrates backend selection:

  • Check for Python side-car — If a FastAPI service is available, it can handle the operation
  • Default to Turf.js — For geometry-only tasks on reasonably sized data
  • Activate DuckDB-WASM — For spatial joins, large datasets, or SQL-required operations

This design delivers two critical benefits:

  • Responsive UI — Small operations use lightweight Turf.js
  • Scalable analysis — Complex work stays in the browser via DuckDB-WASM

Running Client-Side Analysis in GeoLibre

Use these patterns in custom plugins or the browser console:

Buffer with Turf.js:

await runProcessingAlgorithm("buffer", { 
  layer: "myLayer", 
  distance: 2, 
  units: "kilometers" 
});

Spatial join with DuckDB-WASM:

await runProcessingAlgorithm("spatialJoin", {
  layerA: "roads",
  layerB: "parcels",
  predicate: "ST_Intersects"
});

Key Source Files in GeoLibre

File Purpose
packages/processing/src/vector-tools.ts Turf-based algorithms: buffer, centroid, union, simplify
packages/processing/src/topology-tools.ts DuckDB-WASM Spatial for complex topology and joins
apps/geolibre-desktop/src/lib/duckdb-processing.ts DuckDB-WASM wrapper: ensureExtensions, registerGeoJson, query
packages/plugins/src/plugins/maplibre-duckdb.ts MapLibre UI integration for DuckDB debugging
packages/processing/src/runner.ts Backend orchestration: Turf vs DuckDB vs Python side-car

Summary

  • Turf.js handles fast, geometry-only operations on small-to-medium GeoJSON datasets in vector-tools.ts
  • DuckDB-WASM Spatial scales to large data and complex SQL queries via topology-tools.ts and duckdb-processing.ts
  • Automatic backend selection in runner.ts optimizes for responsiveness without sacrificing capability
  • Fully client-side execution preserves privacy and works offline, with no server dependencies required

Frequently Asked Questions

What file formats does GeoLibre's client-side analysis support?

GeoLibre processes data internally as GeoJSON FeatureCollections. The DuckDB-WASM pipeline registers these as temporary tables using registerGeoJson(), enabling SQL queries on any source that can be converted to GeoJSON—including Shapefile, GeoPackage, and FlatGeobuf via the application's import converters.

When does GeoLibre switch from Turf.js to DuckDB-WASM?

The switch happens automatically based on algorithm requirements rather than dataset size. Operations requiring spatial joins, set-based aggregations like ST_Union across many features, or SQL predicates use DuckDB-WASM. Simple per-feature geometry transforms use Turf.js. Future versions may add dynamic size thresholds.

Can I use DuckDB-WASM Spatial queries directly without the processing API?

Yes. The underlying DuckDB instance is accessible through the processing context's duckdb capability. Advanced users can call duckdb.query() directly with raw spatial SQL after ensuring the spatial extension is loaded, though the processing algorithm wrapper handles result conversion and layer management automatically.

Does GeoLibre's client-side analysis work in mobile browsers?

Yes. Both Turf.js and DuckDB-WASM run in any modern browser with WebAssembly support. Performance depends on device memory—DuckDB-WASM excels on desktop but handles moderate datasets on mobile. The WASM runs in a separate thread via Web Workers where available, keeping the map responsive.

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 →