GDAL and a deep C++ stack (PROJ, GEOS, libtiff and more), compiled to WebAssembly for the web and to native modules for iOS and Android. Read, write, and transform raster and vector formats entirely on-device. No server. No upload.
Set up gdal3.js v3 in my project. Detect my bundler and package manager, then: install gdal3.js, the matching @cpp.js/plugin-* (vite / webpack / rspack / rollup / metro), and the platform package (@gdal3.js/wasm for web); create cppjs.config.js at the project root importing "@gdal3.js/wasm/cppjs.config.mjs"; add the cpp.js plugin to my bundler config; then add a minimal example that opens a file with Gdal.open. Reference: https://gdal3.js.org/docs/getting-started/One library, the same API across every runtime. Process raster and vector geodata where the user already is, without standing up a backend or shipping native binaries to every device.
What do you give up by running GDAL in the browser? Not much. Common operations land within about 2x of native GDAL, several on par. And the work runs on the user's device, so you skip the upload and download a backend would need, usually a bigger cost than the wasm overhead itself.
A minimal reprojection pipeline. The same input, the same output, on every supported runtime. Just a different way to read the file in.
// vite.config.ts, one line: plugins: [cppjs()] import 'gdal3.js/Dataset.h'; import { initCppJs, Gdal } from 'gdal3.js/Gdal.h'; const Module = await initCppJs({ useWorker: true, fs: { opfs: true } }); // put the user's file into OPFS; gdal sees it as /opfs/… const dir = await navigator.storage.getDirectory(); const fh = await dir.getFileHandle(file.name, { create: true }); const ws = await fh.createWritable(); await ws.write(file); await ws.close(); const ds = await Gdal.open(`/opfs/${file.name}`); const opts = await Module.toVector('VectorString', ['-f', 'GPKG', '-t_srs', 'EPSG:3857']); const out = await ds.vectorTranslate('/opfs/out.gpkg', opts); await out.close(); // out.gpkg persists in OPFS, survives reloads
// Bundle with @cpp.js/plugin-rollup or -webpack targeting Node, // or use the prebuilt Node build from @gdal3.js/wasm-bundle. import 'gdal3.js/Dataset.h'; import { initCppJs, Gdal } from 'gdal3.js/Gdal.h'; const Module = await initCppJs(); // Node: host filesystem, no VFS hop const ds = await Gdal.open('data/districts.gpkg'); const opts = await Module.toVector('VectorString', ['-f', 'GeoJSON', '-t_srs', 'EPSG:4326']); const out = await ds.vectorTranslate('out/districts.geojson', opts); await out.close();
// metro.config.js: wrap once with the cpp.js Metro plugin. // Native module builds at pod-install / gradle time. Same imports as web: import 'gdal3.js/Dataset.h'; import { initCppJs, Gdal } from 'gdal3.js/Gdal.h'; import * as DocumentPicker from 'expo-document-picker'; import * as FileSystem from 'expo-file-system/legacy'; const Module = await initCppJs(); // real native GDAL over JSI, no wasm const picked = await DocumentPicker.getDocumentAsync(); const path = picked.assets[0].uri.replace('file://', ''); const ds = await Gdal.open(path); const opts = await Module.toVector('VectorString', ['-f', 'GPKG']); const out = await ds.vectorTranslate( `${FileSystem.cacheDirectory.replace('file://', '')}out.gpkg`, opts);
// No bundler on Workers: the self-contained single-threaded edge build, no plugin. // Full 189-driver build is large; size-capped Workers need wasm-bundle-minimal (WIP). import { initCppJs, Gdal } from '@gdal3.js/wasm-bundle'; export default { async fetch(request) { const Module = await initCppJs(); // single-threaded, in-memory FS const bytes = new Uint8Array(await request.arrayBuffer()); await Gdal.fileFromMemBuffer('/vsimem/in.geojson', bytes); const ds = await Gdal.open('/vsimem/in.geojson'); const opts = await Module.toVector('VectorString', ['-f', 'GPKG', '-t_srs', 'EPSG:3857']); const out = await ds.vectorTranslate('/vsimem/out.gpkg', opts); await out.close(); return new Response(Module.FS.readFile('/vsimem/out.gpkg')); }, };
<!-- The stable v2 line is on npm today; drop into any page. --> <script src="https://cdn.jsdelivr.net/npm/gdal3.js@2.8.1/dist/package/gdal3.js"></script> <script> initGdalJs({ path: 'https://cdn.jsdelivr.net/npm/gdal3.js@2.8.1/dist/package', }).then(async (Gdal) => { const f = document.querySelector('input[type=file]').files[0]; const input = await Gdal.open(f); const out = await Gdal.ogr2ogr(input.datasets[0], ['-f', 'GeoJSON', '-t_srs', 'EPSG:4326']); const bytes = await Gdal.getFileBytes(out); }); </script>
All 189 drivers from GDAL's registry are bundled with every release: no à-la-carte builds. The list below is a sample; search the registry for what you need.
gdal3.js has powered geospatial work in browsers and on servers for years. v3 is the next chapter for the same widely used engine.
Most of what's new started as somebody's GitHub issue, with merged contributions from GDAL's own lead maintainer among them. A sample of requests that became features.
The GIS converter app, built on this library, moved to its own domain and was rebuilt with a new design as it grows into a full toolkit. The engine and the privacy are unchanged: your files still never leave the browser.
Drop your files in, get the format you need out. Reproject, warp, repackage, all on-device, your files never leave the browser. Now at its own address with a redesigned interface, free to grow into a full toolkit.
gdal3.js.org
→
geosmith.dev
The things people ask first. If yours isn't here, open an issue on GitHub. We read them all.
v3 is a ground-up rewrite on cpp.js, and the API changed with it. The headlines:
• You import GDAL's headers as modules (import { initCppJs, Gdal } from 'gdal3.js/Gdal.h'), and a bundler plugin (Vite, Webpack, Rollup, Rspack or Metro) wires the wasm, data and worker assets automatically. No more copy-plugin and paths configuration.
• The v2 helpers (Gdal.ogr2ogr(dataset, args)) became dataset methods: ds.vectorTranslate(outPath, opts), plus class-level access to Dataset, Driver and GCP.
• v2 (2.8.x) stays on npm and keeps working; migrate when you're ready.
The full diff lives in the v2 → v3 migration guide.
Not yet over the network. /vsicurl/ needs a curl that speaks browser-fetch, and that work is tracked in issue #67. What works today: fetch the file yourself and hand the bytes to gdal3.js, plus local VSI layers like /vsizip/ (zipped Shapefiles open directly) and in-memory /vsimem/.
The v3 full build is roughly 14 MB gzipped on first load: ~11.5 MB of wasm plus ~2.2 MB of driver/PROJ data. That's the price of the complete driver registry with the new codecs; it loads lazily when you call initCppJs() and is cached after the first run.
If that's too much, slimmer driver subsets are the plan for the minimal bundle, and on React Native the question disappears. GDAL ships inside the app binary, not over the network.
Functionally: it's the same GDAL, with the same option flags. If your script works with ogr2ogr on the command line, the same flags work here.
Performance-wise, in the browser expect roughly 1.5× to 3× the wall-clock time of native, depending on the operation. On iOS and Android there is no wasm penalty at all; v3 runs GDAL as a real native library. In Node, if you need maximum throughput and can install native binaries, a native binding will still be faster; gdal3.js trades that for zero native dependencies.
Yes. After the wasm and data files are cached (first run), the library runs entirely offline, including in a service-worker-cached PWA or a React Native app that has no network. There are no runtime calls to a GDAL server.
gdal3.js is released under the MIT License, but the default build bundles a few LGPL libraries (GEOS, SpatiaLite, libiconv), so the package you ship is LGPL-2.1-or-later by default. For a build with permissive licenses only, override the cpp.js config to exclude those three (with reduced geometry/encoding support). Whether a given build fits your project is for you to confirm against the license terms; the license page has the full breakdown.
This was settled early in the project's history (issue #36, GPL → LGPL by community request).
Uncertain. The 2.8.x line is still on npm and got fixes as recently as April to May 2026 (the JPEG/ZSTD/LERC codecs and a worker memory-leak fix), but continued maintenance isn't guaranteed: new features land in v3 only, and v2 patches depend on time and community PRs. No fixed end-of-life date, but no promise either, so plan to migrate with the migration guide.
GDAL 3.13.1, with PROJ and the rest of the native stack, prebuilt through the cpp.js package registry. Upstream releases are tracked as dependency bumps, so version updates are routine rather than rebuild projects. The v2 line is on GDAL 3.8.x.