npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

leangraph

v1.1.2

Published

SQLite-based graph database with Cypher query support

Readme

LeanGraph

npm version License: MIT TCK

A lightweight, embeddable graph database with full Cypher query support, powered by SQLite.

100% openCypher TCK Compliance — LeanGraph passes all 2,684 test scenarios from the openCypher Technology Compatibility Kit (Neo4j 3.5 baseline). Every Cypher feature that Neo4j 3.5 supports, LeanGraph supports.

Why LeanGraph?

| Feature | LeanGraph | Neo4j | |---------|-----------|-------| | Startup time | Instant | 30+ seconds | | Memory | ~50MB | 1GB+ minimum | | Deployment | Single npm package | JVM + complex setup | | Docker required | No | Typically yes | | Works offline | Yes | Server required | | Backup | Copy the SQLite file | Enterprise license | | Cypher support | Full (Neo4j 3.5 parity) | Full | | Cost | Free, MIT license | Free tier limited |

LeanGraph is ideal for:

  • Production graph workloads with zero infrastructure
  • Neo4j-level queries without Neo4j-level complexity
  • Self-hosted apps where simplicity is a feature
  • Instant local databases for development and testing

Installation

npm install leangraph
npm install -D better-sqlite3

better-sqlite3 is only needed for local and test modes. Production deployments using remote mode don't require it, keeping your node_modules lean and avoiding native rebuilds.

Quick Start

import { LeanGraph } from 'leangraph';

const db = await LeanGraph({ project: 'myapp' });

// Create nodes and relationships
await db.execute(`
  CREATE (alice:User {name: 'Alice'})-[:FOLLOWS]->(bob:User {name: 'Bob'})
`);

// Query the graph
const users = await db.query('MATCH (u:User) RETURN u.name AS name');
console.log(users); // [{ name: 'Alice' }, { name: 'Bob' }]

db.close();

Modes

| Mode | LEANGRAPH_MODE | Behavior | |------|------------------|----------| | Local | unset or local | Embedded SQLite at ./data/{project}.db | | Remote | remote | HTTP connection to LeanGraph server | | Test | test | In-memory SQLite (resets on restart) |

Local Mode (default)

Uses an embedded SQLite database. No server required.

const db = await LeanGraph({ project: 'myapp' });
// Data persists at ./data/myapp.db

Remote Mode

Your code can stay identical for local development and production. Just configure environment variables:

.env

LEANGRAPH_MODE=remote
LEANGRAPH_API_KEY=lg_xxx
// Same code works locally (dev) and remotely (production)
const db = await LeanGraph({ project: 'myapp' });

When LEANGRAPH_MODE=remote is set, LeanGraph automatically connects via HTTP instead of embedded LeanGraph.

Tip: Remote mode doesn't use better-sqlite3, so installing it as a dev dependency speeds up production deploys by skipping native module compilation.

Test Mode

Uses an in-memory SQLite database that resets when the process exits.

const db = await LeanGraph({ mode: 'test', project: 'myapp' });

Configuration

interface LeanGraphOptions {
  mode?: "local" | "remote" | "test";
  project?: string;
  url?: string;
  apiKey?: string;
  dataPath?: string;
}

| Option | Environment Variable | Default | Description | |--------|---------------------|---------|-------------| | mode | LEANGRAPH_MODE | "local" | local, remote, or test | | project | LEANGRAPH_PROJECT | — | Project name (required) | | url | LEANGRAPH_URL | "https://leangraph.io" | Server URL (remote mode) | | apiKey | LEANGRAPH_API_KEY | — | API key (remote mode) | | dataPath | LEANGRAPH_DATA_PATH | "./data" | Data directory (local mode) |

Options passed to LeanGraph() take precedence over environment variables.

API Reference

LeanGraph(options): Promise<LeanGraphClient>

Create a new LeanGraph client. Returns a promise that resolves to a client instance.

db.query<T>(cypher, params?): Promise<T[]>

Execute a Cypher query and return results as an array.

const users = await db.query<{ name: string; age: number }>(
  'MATCH (u:User) WHERE u.age > $minAge RETURN u.name AS name, u.age AS age',
  { minAge: 21 }
);
// users = [{ name: 'Alice', age: 25 }, { name: 'Bob', age: 30 }]

db.execute(cypher, params?): Promise<void>

Execute a mutating query (CREATE, SET, DELETE, MERGE) without expecting return data.

await db.execute('CREATE (n:User {name: $name, email: $email})', {
  name: 'Alice',
  email: '[email protected]'
});

db.queryRaw<T>(cypher, params?): Promise<QueryResponse<T>>

Execute a query and return the full response including metadata.

const response = await db.queryRaw('MATCH (n) RETURN n LIMIT 10');
console.log(response.meta.count);   // Number of rows
console.log(response.meta.time_ms); // Query execution time in ms
console.log(response.data);         // Array of results

Convenience Methods

Thin wrappers around common Cypher operations:

db.createNode(label, properties?): Promise<string>
db.getNode(label, filter): Promise<Record<string, unknown> | null>
db.updateNode(id, properties): Promise<void>
db.deleteNode(id): Promise<void>
db.createEdge(sourceId, type, targetId, properties?): Promise<void>

db.health(): Promise<{ status: string; timestamp: string }>

Check server health. In development mode, always returns { status: 'ok', ... }.

db.close(): void

Close the client and release resources. Always call this when done.

const db = await LeanGraph({ ... });
try {
  // ... use db
} finally {
  db.close();
}

Common Patterns

CRUD Operations

// Create
await db.execute(
  'CREATE (u:User {name: $name, email: $email})',
  { name: 'Alice', email: '[email protected]' }
);

// Read
const [user] = await db.query<{ name: string; email: string }>(
  'MATCH (u:User {email: $email}) RETURN u.name AS name, u.email AS email',
  { email: '[email protected]' }
);

// Update
await db.execute(
  'MATCH (u:User {email: $email}) SET u.verified = true',
  { email: '[email protected]' }
);

// Delete
await db.execute(
  'MATCH (u:User {email: $email}) DETACH DELETE u',
  { email: '[email protected]' }
);

Parameterized Queries

Always use parameters for user input:

// Good - parameterized
const users = await db.query(
  'MATCH (u:User) WHERE u.email = $email RETURN u',
  { email: userInput }
);

// Bad - string interpolation (injection risk)
const users = await db.query(`MATCH (u:User) WHERE u.email = '${userInput}' RETURN u`);

Typed Results

interface User {
  name: string;
  email: string;
}

const users = await db.query<User>(
  'MATCH (u:User) RETURN u.name AS name, u.email AS email'
);

users[0].name;  // TypeScript knows this is string

Relationships

// Create a relationship
await db.execute(`
  MATCH (a:User {name: $from}), (b:User {name: $to})
  CREATE (a)-[:FOLLOWS {since: $since}]->(b)
`, { from: 'Alice', to: 'Bob', since: '2024-01-01' });

// Query relationships
const following = await db.query<{ name: string }>(`
  MATCH (:User {name: $name})-[:FOLLOWS]->(friend:User)
  RETURN friend.name AS name
`, { name: 'Alice' });

// Variable-length paths (1-3 hops)
const connections = await db.query<{ name: string }>(`
  MATCH (:User {name: $name})-[:FOLLOWS*1..3]->(connection:User)
  RETURN DISTINCT connection.name AS name
`, { name: 'Alice' });

Upsert with MERGE

await db.execute(`
  MERGE (u:User {email: $email})
  ON CREATE SET u.name = $name, u.createdAt = datetime()
  ON MATCH SET u.lastSeen = datetime()
`, { email: '[email protected]', name: 'Alice' });

Batch Insert with UNWIND

const users = [
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' },
];

await db.execute(`
  UNWIND $users AS data
  CREATE (u:User {name: data.name, email: data.email})
`, { users });

Error Handling

import { LeanGraph, LeanGraphError } from 'leangraph';

try {
  await db.query('MATCH (n:User RETURN n'); // syntax error
} catch (err) {
  if (err instanceof LeanGraphError) {
    console.error(`Query failed: ${err.message}`);
    console.error(`Position: line ${err.line}, column ${err.column}`);
  }
}

Testing

Use test mode for fast, isolated tests:

import { LeanGraph } from 'leangraph';

const db = await LeanGraph({ mode: 'test', project: 'test' });

// Tests run against in-memory database
await db.execute('CREATE (u:User {name: $name})', { name: 'Test' });
const [user] = await db.query('MATCH (u:User) RETURN u.name AS name');
assert(user.name === 'Test');

db.close(); // In-memory DB is discarded

Cypher Quick Reference

Supported Clauses

| Clause | Example | |--------|---------| | CREATE | CREATE (n:User {name: 'Alice'}) | | MATCH | MATCH (n:User) RETURN n | | OPTIONAL MATCH | OPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN m | | MERGE | MERGE (n:User {email: $email}) | | WHERE | WHERE n.age > 21 AND n.active = true | | SET | SET n.name = 'Bob', n.updated = true | | DELETE | DELETE n | | DETACH DELETE | DETACH DELETE n | | RETURN | RETURN n.name AS name, count(*) AS total | | WITH | WITH n, count(*) AS cnt WHERE cnt > 1 | | UNWIND | UNWIND $list AS item CREATE (n {value: item}) | | UNION / UNION ALL | MATCH (n:A) RETURN n UNION MATCH (m:B) RETURN m | | ORDER BY | ORDER BY n.name DESC | | SKIP / LIMIT | SKIP 10 LIMIT 5 | | DISTINCT | RETURN DISTINCT n.category | | CASE/WHEN | RETURN CASE WHEN n.age > 18 THEN 'adult' ELSE 'minor' END | | CALL | CALL db.labels() YIELD label RETURN label |

Operators

| Category | Operators | |----------|-----------| | Comparison | =, <>, <, >, <=, >= | | Logical | AND, OR, NOT | | String | CONTAINS, STARTS WITH, ENDS WITH | | List | IN | | Null | IS NULL, IS NOT NULL | | Pattern | EXISTS | | Arithmetic | +, -, *, /, % |

Functions

Aggregation: COUNT, SUM, AVG, MIN, MAX, COLLECT

Scalar: ID, coalesce

String: toUpper, toLower, trim, substring, replace, toString, split

List: size, head, last, tail, keys, range

Node/Relationship: labels, type, properties

Math: abs, ceil, floor, round, rand, sqrt

Date/Time: date, datetime, timestamp

Variable-Length Paths

-- Find friends of friends (1 to 3 hops)
MATCH (a:User {name: 'Alice'})-[:KNOWS*1..3]->(b:User)
RETURN DISTINCT b.name

Procedures

-- List all labels
CALL db.labels() YIELD label RETURN label

-- List all relationship types
CALL db.relationshipTypes() YIELD type RETURN type

-- List all property keys
CALL db.propertyKeys() YIELD key RETURN key

Running the Server (Production)

For production deployments, run a dedicated server:

# Start the server
npx leangraph serve --port 3000 --data ./data

# Or with custom host binding
npx leangraph serve --port 3000 --host 0.0.0.0 --data ./data

Creating Projects

# Create a new project (generates API key)
npx leangraph create myapp --data ./data

# Output:
#   [created] production/myapp.db
#   API Key: lg_abc123...

CLI Reference

# Server
leangraph serve [options]
  -p, --port <port>     Port to listen on (default: 3000)
  -d, --data <path>     Data directory (default: /var/data/leangraph)
  -H, --host <host>     Host to bind to (default: localhost)
  -b, --backup <path>   Backup directory (enables backup endpoints)

# Project management
leangraph create <project>   Create new project with API keys
leangraph delete <project>   Delete project (use --force)
leangraph list               List all projects

# Environment management
leangraph clone <project> --from <env> --to <env>   Copy between environments
leangraph wipe <project> --env <env>                Clear environment database

# Direct queries
leangraph query <env> <project> "CYPHER"

# Backup
leangraph backup [options]
  -o, --output <path>   Backup directory
  -p, --project <name>  Backup specific project
  --status              Show backup status

# API keys
leangraph apikey add <project>
leangraph apikey list
leangraph apikey remove <prefix>

Advanced Usage

Direct Database Access

For advanced use cases, you can access the underlying components:

import { GraphDatabase, Executor, parse, translate } from 'leangraph';

// Direct database access
const db = new GraphDatabase('./my-database.db');
db.initialize();

const executor = new Executor(db);
const result = executor.execute('MATCH (n) RETURN n LIMIT 10');

db.close();

// Parse Cypher to AST
const parseResult = parse('MATCH (n:User) RETURN n');
if (parseResult.success) {
  console.log(parseResult.query);
}

// Translate AST to SQL
const translation = translate(parseResult.query, {});
console.log(translation.statements);

Running a Custom Server

import { createServer } from 'leangraph';
import { serve } from '@hono/node-server';

const { app, dbManager } = createServer({
  dataPath: './data',
  apiKeys: {
    'my-api-key': { project: 'myapp', env: 'production' }
  }
});

serve({ fetch: app.fetch, port: 3000 });

Known Limitations

Large Integer Precision

JavaScript cannot precisely represent integers larger than Number.MAX_SAFE_INTEGER (9,007,199,254,740,991). Integers beyond this range will lose precision, which can cause unexpected behavior when comparing values.

Example of the problem:

// These two different numbers become equal in JavaScript!
const a = 4611686018427387905;
const b = 4611686018427387900;
console.log(a === b); // true (both round to 4611686018427388000)

Workaround: Use strings for large integer IDs:

// Instead of:
CREATE (u:User {id: 4611686018427387905})

// Use strings:
CREATE (u:User {id: '4611686018427387905'})
MATCH (u:User {id: '4611686018427387905'}) RETURN u

This limitation affects all JavaScript-based systems, including Neo4j's JavaScript driver. For IDs that may exceed the safe integer range, string representation is the recommended approach.

License

MIT - Conrad Lelubre