Embedding Traverse WASM in a Web App
Install the npm package, instantiate the engine, run queries, persist to OPFS — everything you need to drop Traverse into your own web app.
This page is for integrators. If you just want to use Traverse in a browser tab, open traverse.truespar.com — that's our hosted demo, no code required.
Installation
npm install @truespar/traverse-wasm
The package ships:
TraverseDb— the main async class.- A bundled Web Worker (
worker.js) that hosts the WASM module. - The compiled WASM binary (~5 MB).
- TypeScript declarations (
index.d.ts) auto-picked-up by TS / Vite / webpack.
Instantiation
Minimal example
import { TraverseDb } from '@truespar/traverse-wasm'
const db = await TraverseDb.open({
numThreads: Math.min(navigator.hardwareConcurrency, 16),
})
console.log('engine version:', db.version)
const r = await db.query('MATCH (n) RETURN count(n) AS n')
console.log(r.rows[0]) // [0]
The call initializes the engine and returns a ready-to-use instance. The bundle is cached on first load; subsequent opens are fast.
Optional parameters
| Option | Default | Description |
|---|---|---|
numThreads | min(hardwareConcurrency, 16) | Number of threads the engine may use. 1 works but is slow on algorithm runs. |
workerUrl | bundled | Override only if you need a custom hosting location for the worker script. |
name | — | If set, restores the named database from browser storage on open. |
Vite
Vite needs two tweaks to bundle the package correctly:
// vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig({
// Treat .wasm as an emitted asset.
assetsInclude: ['**/*.wasm'],
// Required so Vite bundles the WASM binary correctly.
optimizeDeps: {
exclude: ['@truespar/traverse-wasm'],
},
server: {
// Required so the engine can use threads during development.
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin',
},
fs: {
// If you install via `file:` (monorepo), let Vite read outside
// the project root.
allow: ['..'],
},
},
})
Cross-origin isolation
SharedArrayBuffer — the thread pool's backbone — is only available on cross-origin-isolated pages. Your hosting must send both headers on every response:
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Copy-paste-ready snippets:
Vercel (vercel.json)
{
"headers": [{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" }
]
}]
}
Netlify (_headers)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
Cloudflare Workers / Pages (_headers in /public)
Same syntax as Netlify above.
nginx
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
Apache (.htaccess)
Header always set Cross-Origin-Embedder-Policy "require-corp"
Header always set Cross-Origin-Opener-Policy "same-origin"
IIS (web.config)
<httpProtocol>
<customHeaders>
<add name="Cross-Origin-Embedder-Policy" value="require-corp" />
<add name="Cross-Origin-Opener-Policy" value="same-origin" />
</customHeaders>
</httpProtocol>
If the headers are missing, TraverseDb.open() throws a clear "this browser blocks SharedArrayBuffer" error rather than failing deep inside the WASM init.
Running Cypher
Simple query
const r = await db.query('MATCH (n:Person) RETURN n.name AS name LIMIT 10')
for (const [name] of r.rows) console.log(name)
Parameters
const r = await db.query(
'MATCH (n:Person {age: $age}) RETURN n',
{ age: 30 },
)
Per-query timeout and cancellation
const controller = new AbortController()
setTimeout(() => controller.abort(), 5_000) // hard cancel after 5s
try {
const r = await db.query(
'CALL traverse.pageRank.stream({maxIterations: 50}) YIELD nodeId, score',
null,
{ signal: controller.signal, timeoutMs: 30_000 },
)
} catch (err) {
if (err.name === 'AbortError') console.log('cancelled')
else throw err
}
QueryResponse shape
interface QueryResponse {
columns: string[] // column names from RETURN
rows: unknown[][] // one row per result
total_rows: number
truncated: boolean
nodes: unknown[] // distinct hydrated nodes
edges: unknown[] // distinct hydrated edges
entities_truncated: boolean
query_type: 'Read' | 'Write' | 'ReadWrite' | 'Schema'
stats: {
nodes_created: number
nodes_deleted: number
relationships_created: number
relationships_deleted: number
properties_set: number
labels_added: number
labels_removed: number
indexes_added: number
indexes_removed: number
constraints_added: number
constraints_removed: number
}
plan?: unknown // EXPLAIN / PROFILE only
time_ms: number
parsing_time_ms: number
planning_time_ms: number
execution_time_ms: number
}
Nodes and edges are tagged JSON objects matching the HTTP server's shape:
{ _type: "node", id: 42, labels: ["Person"], properties: { name: "Alice" } }
{ _type: "edge", id: 7, type: "KNOWS", source: 42, target: 51, properties: {} }
{ _type: "path", nodes: [...], edges: [...] }
Algorithm catalog and schema
// 32+ GDS procedures, same shape the HTTP /api/algorithms returns.
const { algorithms } = await db.algorithms()
algorithms.forEach(a => console.log(a.name, a.modes))
// Labels, relationship types, property keys, indexes, constraints.
const schema = await db.schema()
console.log(schema.labels) // ['Person', 'Movie', ...]
console.log(schema.relationship_types) // ['KNOWS', 'ACTED_IN', ...]
Importing data
// Multi-statement Cypher import (same shape as /api/import).
const result = await db.importCypher(`
CREATE (a:Person {name: 'Alice', age: 30})
CREATE (b:Person {name: 'Bob', age: 28})
CREATE (a)-[:KNOWS]->(b)
`)
console.log(result.stats) // counters per the QueryStats shape above
Persistence: OPFS
Databases live in the browser's Origin Private File System (OPFS) as .tvdb binary blobs. The format is byte-for-byte identical to what the native Traverse server writes.
Save to OPFS
// After making changes, persist under a name.
await db.commit('my-database')
// → { ok: true, bytes: 12345 }
Restore from OPFS
const result = await db.load('my-database')
if (result.ok) {
console.log(`restored ${result.nodes} nodes / ${result.edges} edges`)
} else if (result.missing) {
console.log('no database with that name')
}
List + delete
const dbs = await db.listDatabases()
// → [{ name: 'my-database', size: 12345, lastModified: 1715740800000 }, ...]
await db.deleteDatabase('old-one')
Round-trip with the native server
Export the current graph as a .tvdb byte buffer, hand it to the user to download, or upload to a server:
const bytes = await db.exportTvdb()
// Save to disk via the File API.
const blob = new Blob([bytes], { type: 'application/octet-stream' })
const url = URL.createObjectURL(blob)
Object.assign(document.createElement('a'), { href: url, download: 'my-graph.tvdb' }).click()
// Or load bytes the user uploaded.
const fromUser: Uint8Array = ... // e.g. from <input type="file"> → arrayBuffer()
await db.loadTvdb(fromUser)
The same .tvdb file loads cleanly in the native traverse-server via --data or the HTTP /api/databases/load endpoint, and vice versa.
Tear-down
db.close() // terminates the worker; pending requests reject with AbortError
Browser support
| Browser | Required | Notes |
|---|---|---|
| Chrome / Edge | v95+ | Cross-origin isolation must be enabled on the hosting page. |
| Firefox | v98+ | Same. |
| Safari | v16.4+ | Same; older Safari (and some locked-down corporate profiles) won't expose SharedArrayBuffer at all. |
Detect support before opening:
if (typeof SharedArrayBuffer === 'undefined') {
// The page isn't cross-origin-isolated, or the browser is too old.
// Show your fallback (or a link to truespar.com to download the
// native build).
}
Limits
- ~4 GB linear memory. wasm32 ceiling; practical working set is closer to 2 GB before fragmentation hurts. A
~1.8M-edge graph (~100 MB.tvdb) fits comfortably. - OPFS is per-origin. Subdomains don't share storage.
- One graph in memory. Switching databases is commit + load — both fast, but not concurrent.
- No Bolt / gRPC. The browser tab is the engine; external clients aren't supported. Use the native server if you need that.
TypeScript
The package ships .d.ts declarations — nothing extra to install. All return types are typed; QueryResponse and friends are exported.
import {
TraverseDb,
type QueryResponse,
type AlgorithmsResponse,
type SchemaResponse,
} from '@truespar/traverse-wasm'
Reference
- npm package
- Browser (WASM) overview — what shipping in WASM means and when to use it
- Cypher reference — the query language available in both modes
- traverse.truespar.com — our hosted demo, runs the same code