GDAL 3.13 with its driver registry, 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. No native install.
Set up gdal3.js v3 (beta) in my project. Detect my bundler and package manager, then: install gdal3.js@beta, 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 three runtimes. Process raster and vector geodata where the user already is, without standing up a backend or shipping native binaries to every device.
gdal_translate, ogr2ogr, gdalwarp, gdal_rasterize, and (new in v3) gdalbuildvrt, gdaldem, gdal_grid, GCP georeferencing and the class-level Dataset/Driver API.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);
<!-- 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>
The driver registry of the GDAL 3.13 build is bundled with every release: no à-la-carte builds. The list below is a sample; search the registry for what you need.
gdal3.js targets every JavaScript environment that has a file system or a buffer. The same exports work across all of them; only the entry-point import path differs.
| Environment | Read | Write | OPFS / FS | Workers | Bundle |
|---|---|---|---|---|---|
| Browser CHROME 90+ · FIREFOX 89+ · SAFARI 15.2+ |
● | ● | ●OPFS | ● | ESM / CJS / UMD |
| Node.js v18+ · v20 LTS RECOMMENDED |
● | ● | ●node:fs | ● | ESM / CJS |
| React Native NEW NEW ARCHITECTURE · EXPO & BARE |
● | ● | ●device FS | ◐native threads | Native module (JSI) |
| Edge runtimes EXPERIMENTAL · DEDICATED EDGE BUILD |
● | ◐ | ○in-memory | ○ | check platform wasm size limits |
A few patterns we see most often. The library doesn't care what you put on top of it. Converter, viewer, pipeline, app. It just exposes GDAL.
gdalinfo client-side: CRS, bounds, band stats, feature counts. Reject malformed files at upload time, not after they're stored.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 so it can grow into a full toolkit. Same tool, same files, same privacy guarantees. Just a different URL.
168 formats in, the format you need out. Reproject, warp, repackage. Files never leave the device. Now lives at its own address so the converter team can ship faster without disturbing the library.
gdal3.js.org
→
geosmith.dev
Start with the path that matches what you're trying to do: get something running, look up a function, or understand how the wasm side works.
Released under the MIT License. The prebuilt build is LGPL only because of the libraries it bundles. You can drop those for an MIT/BSD-only build.
The things people ask first. If yours isn't here, open an issue on GitHub. We read them all.
The converter that used to live at gdal3.js.org moved to geosmith.dev. Nothing else changed. Same files, same wasm pipeline, same privacy guarantees (your data never leaves the device).
We split it out because the converter is becoming a full geospatial toolkit, with its own roadmap. Keeping it separate lets the library stay small and focused on being a great wasm GDAL build, and lets the app team ship on their own schedule.
If you bookmarked the old URL, follow the big orange button at the top of this page, or go directly to geosmith.app.
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.
Into a virtual filesystem, the single most-asked question in the issue tracker. In the browser there is no real disk, so GDAL writes into an in-memory or OPFS-backed FS. You pick the output path (e.g. /opfs/out.gpkg), then read it back as bytes, trigger a download, or leave it in OPFS where it survives page reloads.
The FS is a first-class API now: readDir, unlink, rename, copyFile, mkdir, and clearFS() to reset between conversions (community PR #109). In Node and React Native there's no hop at all: GDAL reads and writes your real filesystem directly.
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/.
Fixed. Older builds shipped libtiff without the JPEG, ZSTD and LERC codecs, so compressed GeoTIFFs and COGs failed to open. Community PRs (#105, #107, #108) wired libjpeg-turbo, zstd and LERC into the build; v3 includes all three, for reading and writing (-co COMPRESS=JPEG|ZSTD|LERC|LERC_ZSTD).
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. That's still fine for a commercial, closed-source app. Your application code stays closed; LGPL only asks that users can replace the LGPL parts and that changes to those libraries are shared. Need a fully permissive build? Override the cpp.js config to exclude GEOS, SpatiaLite and libiconv; everything else (GDAL, PROJ, …) is MIT/BSD, so there's no copyleft obligation (with reduced geometry/encoding support). See the license page for the full breakdown.
This was settled early in the project's history (issue #36, GPL → LGPL by community request).