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):
- Fetch the input layer from context
- Execute the Turf helper with user parameters
- 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:
- Ensure the spatial extension is loaded —
await duckdb.ensureExtensions(["spatial"]) - Register GeoJSON as a DuckDB table —
duckdb.registerGeoJson(tagFeatureIndexes(fc)) - Execute spatial SQL queries — Standard PostGIS-compatible functions like
ST_Union,ST_Intersects - Convert results back to GeoJSON — Rows become a
FeatureCollectionfor 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.tsandduckdb-processing.ts - Automatic backend selection in
runner.tsoptimizes 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →