How CereusDB (Apache Sedona WASM) Enables Client-Side Spatial SQL Execution in GeoLibre

GeoLibre runs spatial SQL entirely in the browser using CereusDB, the WebAssembly build of Apache Sedona, through dynamic WASM loading, singleton instance management, and an exclusive execution queue.

The opengeos/GeoLibre project delivers a complete client-side spatial SQL engine by integrating CereusDB—the WASM port of Apache Sedona. Unlike traditional setups requiring a remote database server, GeoLibre executes spatial SQL queries directly in the browser using a ~40 MB WebAssembly binary that loads on demand. This architecture eliminates network round-trips for query processing while maintaining compatibility with standard PostGIS-style spatial functions.

Dynamic WASM Loading with Vite Integration

GeoLibre minimizes initial bundle size by deferring the CereusDB WASM binary until first use. The build process emits the binary as a hashed asset, imported via Vite's ?url syntax.

In apps/geolibre-desktop/src/lib/cereus-loader.ts (lines 24-55), the loader handles this initialization:

// Dynamic import pattern - WASM only fetched when needed
import cereusWasmUrl from '@cereusdb/standard/dist/cereus.wasm?url';

export async function loadCereusDb(): Promise<CereusInstance> {
  const { CereusDB } = await import('@cereusdb/standard');
  return CereusDB.create({ wasmUrl: cereusWasmUrl });
}

This approach keeps the initial application bundle lightweight. The @cereusdb/standard package loads dynamically, and the WASM binary fetches asynchronously only when the user first opens a SQL workspace.

Singleton Instance Pattern for Memory Efficiency

The workspace implements a memoized promise pattern to prevent redundant WASM fetches. In apps/geolibre-desktop/src/lib/sedona-workspace.ts (lines 48-60):

  • A dbPromise variable caches the initialization promise
  • The ~40 MB WASM module downloads once per browser session
  • Failed fetches automatically clear the promise, enabling graceful retries
// Singleton pattern from sedona-workspace.ts
let dbPromise: Promise<CereusInstance> | undefined;

async function getDb(): Promise<CereusInstance> {
  if (!dbPromise) {
    dbPromise = loadCereusDb().catch(err => {
      dbPromise = undefined;  // Clear on failure for retry
      throw err;
    });
  }
  return dbPromise;
}

Exclusive Execution Queue for Thread Safety

CereusDB's WASM engine processes one operation at a time. To prevent race conditions during concurrent queries, GeoLibre implements an exclusive queue at lines 65-74 of sedona-workspace.ts:

import { Mutex } from 'async-mutex';

const dbMutex = new Mutex();

async function runExclusive<T>(fn: () => Promise<T>): Promise<T> {
  return dbMutex.runExclusive(fn);  // Queues concurrent operations
}

This guarantees that table registrations and query executions never interleave—a critical safeguard when multiple SQL dialogs run simultaneously.

Registering GeoJSON Layers as Spatial Tables

CereusDB exposes spatial data through standard SQL tables. GeoLibre converts in-memory GeoJSON layers into queryable tables via two-phase registration (lines 76-84, 99-107, 124-136):

// Phase 1: Register raw GeoJSON
db.registerGeoJSON("layer_source", geojsonFeatureCollection);
// Creates table with: geometry (WKT text), properties (JSON string)

// Phase 2: Create parsing view
await db.sqlJSON(`
  CREATE OR REPLACE VIEW layer AS
  SELECT ST_GeomFromText(geometry) AS geometry, properties
  FROM layer_source
`);

The registerGeoJSON() method stores geometries as Well-Known Text (WKT) and properties as JSON strings. A subsequent view applies ST_GeomFromText to produce proper geometry columns usable with spatial predicates.

Schema Discovery via Arrow IPC Streams

Before rendering results, GeoLibre discovers column names and geometry types. At lines 20-33 of sedona-workspace.ts, the engine:

  1. Executes the full statement via db.sql() returning an Arrow IPC stream
  2. Extracts schema metadata from the Arrow table
  3. Detects geometry columns through GeoArrow extension metadata or name-based heuristics
const arrowTable = await db.sql(query);
const schema = arrowTable.schema;

// GeoArrow detection
const geoColumn = schema.fields.find(f => 
  f.metadata?.get('ARROW:extension:name') === 'geoarrow.geometry'
);

This schema discovery enables dynamic result grid generation without pre-defined table structures.

Query Execution with Dual Geometry Formats

GeoLibre's query wrapper (lines 64-90) produces results in parallel formats for different consumption paths:

const wrappedQuery = `
  SELECT 
    -- WKT for display grid
    ST_AsText(geometry) as geometry,
    -- Hidden GeoJSON for layer creation
    ST_AsGeoJSON(geometry) as __geojson_geometry,
    properties ->> 'name' as name
  FROM (${userQuery}) AS __geolibre_sql_subquery
`;

const result = await db.sqlJSON(wrappedQuery);

The execution flow:

  • Subquery wrapping isolates user SQL while injecting geometry conversions
  • ST_AsText produces human-readable WKT for the results grid
  • ST_AsGeoJSON provides machine-readable geometry for the "Add as Layer" button
  • db.sqlJSON() returns flattened rows with expanded properties columns

Complete Client-Side Workflow Example

import { loadCereusDb } from "./lib/cereus-loader";

// 1. Initialize (first call triggers WASM fetch)
const db = await loadCereusDb();

// 2. Load sample data
const cities = {
  type: "FeatureCollection",
  features: [{
    type: "Feature",
    geometry: { type: "Point", coordinates: [-122.4194, 37.7749] },
    properties: { name: "San Francisco", population: 873965 }
  }]
};

// 3. Register and prepare spatial table
db.registerGeoJSON("cities_raw", cities);
await db.sqlJSON(`
  CREATE OR REPLACE VIEW cities AS
  SELECT ST_GeomFromText(geometry) AS geom, properties
  FROM cities_raw
`);

// 4. Execute spatial SQL entirely in browser
const nearby = await db.sqlJSON(`
  SELECT 
    properties ->> 'name' AS city,
    ST_Distance(geom, ST_GeomFromText('POINT(-122.5 37.8)')) AS dist_km
  FROM cities
  WHERE ST_DWithin(geom, ST_GeomFromText('POINT(-122.5 37.8)'), 0.5)
`);
// Returns: [{ city: "San Francisco", dist_km: 8.92 }]

Automatic Fallback Routing

When the optional Python sidecar (for heavier analytics) is unavailable, GeoLibre transparently routes all queries to CereusDB. The runSedonaQuery function at lines 84-98 of sedona-workspace.ts implements this engine selection logic—users experience seamless degradation without configuration changes.

Build-Time Loader Variants

The project supports two WASM distribution strategies controlled by environment variables:

Loader File Use Case
Bundled cereus-loader.ts Self-contained builds, air-gapped environments
CDN cereus-loader.cdn.ts Reduced bundle size, faster initial load

Vite configuration at vite.config.ts swaps implementations via GEOLIBRE_CEREUS_CDN flag:

// Conditional alias resolution
resolve: {
  alias: process.env.GEOLIBRE_CEREUS_CDN
    ? { './cereus-loader': './cereus-loader.cdn.ts' }
    : {}
}

Summary

  • CereusDB brings Apache Sedona's spatial SQL engine to the browser via WebAssembly
  • Dynamic loading with Vite's ?url syntax defers the ~40 MB WASM payload until first use
  • Singleton pattern with dbPromise memoization prevents redundant module fetches
  • Mutex-based queueing (runExclusive) ensures single-threaded WASM safety
  • Two-phase registration converts GeoJSON to WKT-backed tables with parsed geometry views
  • Arrow IPC streams enable automatic schema and geometry column discovery
  • Dual-format results provide both WKT (display) and GeoJSON (layer creation) outputs
  • Transparent fallback routes queries to CereusDB when Python sidecar is unavailable

Frequently Asked Questions

What is CereusDB and how does it relate to Apache Sedona?

CereusDB is the official WebAssembly build of Apache Sedona, compiling the JVM-based spatial analytics engine into browser-executable WASM. It exposes the same spatial SQL functions (ST_Intersects, ST_Buffer, ST_Distance, etc.) without requiring a Java runtime or server infrastructure.

How large is the CereusDB WASM binary and when does it load?

The WASM binary is approximately 40 MB compressed. GeoLibre loads it only on first SQL workspace use through dynamic import(), keeping the initial application bundle small. The singleton pattern ensures one fetch per browser session regardless of how many queries execute.

Can CereusDB handle concurrent spatial queries?

No—the WASM engine is single-threaded. GeoLibre wraps all database operations with Mutex.runExclusive() to serialize access. Concurrent queries queue automatically; users experience sequential execution without data corruption risks.

What spatial data formats does CereusDB support?

CereusDB ingests GeoJSON via registerGeoJSON(), stores geometries internally as WKT, and outputs results through ST_AsText(), ST_AsGeoJSON(), or GeoArrow-encoded Arrow tables. The engine supports the full Sedona spatial predicate and function library.

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 →