🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

leangraph

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

leangraph

SQLite-based graph database with Cypher query support

latest
Source
npmnpm
Version
1.2.2
Version published
Weekly downloads
42
-76.54%
Maintainers
1
Weekly downloads
 
Created
Source

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?

FeatureLeanGraphNeo4j
Startup timeInstant30+ seconds
Memory~50MB1GB+ minimum
DeploymentSingle npm packageJVM + complex setup
Docker requiredNoTypically yes
Works offlineYesServer required
BackupCopy the SQLite fileEnterprise license
Cypher supportFull (Neo4j 3.5 parity)Full
CostFree, MIT licenseFree 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

ModeLEANGRAPH_MODEBehavior
Localunset or localEmbedded SQLite at ./data/{project}.db
RemoteremoteHTTP connection to LeanGraph server
TesttestIn-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;
}
OptionEnvironment VariableDefaultDescription
modeLEANGRAPH_MODE"local"local, remote, or test
projectLEANGRAPH_PROJECTProject name (required)
urlLEANGRAPH_URL"https://leangraph.io"Server URL (remote mode)
apiKeyLEANGRAPH_API_KEYAPI key (remote mode)
dataPathLEANGRAPH_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: 'alice@example.com'
});

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: 'alice@example.com' }
);

// 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: 'alice@example.com' }
);

// Update
await db.execute(
  'MATCH (u:User {email: $email}) SET u.verified = true',
  { email: 'alice@example.com' }
);

// Delete
await db.execute(
  'MATCH (u:User {email: $email}) DETACH DELETE u',
  { email: 'alice@example.com' }
);

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: 'alice@example.com', name: 'Alice' });

Batch Insert with UNWIND

const users = [
  { name: 'Alice', email: 'alice@example.com' },
  { name: 'Bob', email: 'bob@example.com' },
];

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

ClauseExample
CREATECREATE (n:User {name: 'Alice'})
MATCHMATCH (n:User) RETURN n
OPTIONAL MATCHOPTIONAL MATCH (n)-[:KNOWS]->(m) RETURN m
MERGEMERGE (n:User {email: $email})
WHEREWHERE n.age > 21 AND n.active = true
SETSET n.name = 'Bob', n.updated = true
DELETEDELETE n
DETACH DELETEDETACH DELETE n
RETURNRETURN n.name AS name, count(*) AS total
WITHWITH n, count(*) AS cnt WHERE cnt > 1
UNWINDUNWIND $list AS item CREATE (n {value: item})
UNION / UNION ALLMATCH (n:A) RETURN n UNION MATCH (m:B) RETURN m
ORDER BYORDER BY n.name DESC
SKIP / LIMITSKIP 10 LIMIT 5
DISTINCTRETURN DISTINCT n.category
CASE/WHENRETURN CASE WHEN n.age > 18 THEN 'adult' ELSE 'minor' END
CALLCALL db.labels() YIELD label RETURN label
CREATE INDEXCREATE INDEX ON (property)
DROP INDEXDROP INDEX idx_name
CREATE CONSTRAINTCREATE CONSTRAINT ON (n:Label) ASSERT n.prop IS UNIQUE
DROP CONSTRAINTDROP CONSTRAINT constraint_name

Operators

CategoryOperators
Comparison=, <>, <, >, <=, >=
LogicalAND, OR, NOT
StringCONTAINS, STARTS WITH, ENDS WITH
ListIN
NullIS NULL, IS NOT NULL
PatternEXISTS
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

Indexes

Create indexes on frequently-queried properties to improve performance:

-- Create index (auto-named idx_email)
CREATE INDEX ON (email)

-- Create index with custom name
CREATE INDEX idx_user_email ON (email)

-- Drop index
DROP INDEX idx_user_email

Note: The :Label syntax is supported for Neo4j compatibility but labels are ignored—all indexes are global across nodes.

-- These are equivalent (label is ignored)
CREATE INDEX ON (email)
CREATE INDEX ON :User(email)

Built-in indexes (created automatically):

  • Node primary label lookups
  • Edge type, source, and target for traversal

Constraints

Create unique constraints to enforce data integrity:

-- Create unique constraint (auto-named constraint_User_email_unique)
CREATE CONSTRAINT ON (n:User) ASSERT n.email IS UNIQUE

-- Create constraint with custom name
CREATE CONSTRAINT unique_user_email ON (u:User) ASSERT u.email IS UNIQUE

-- Drop constraint
DROP CONSTRAINT unique_user_email

Unique constraints:

  • Enforce uniqueness per label (not global)
  • Automatically create an index for the property
  • Reject duplicate values with a clear error message
await db.execute('CREATE CONSTRAINT ON (u:User) ASSERT u.email IS UNIQUE');
await db.execute('CREATE (u:User {email: "alice@example.com"})');

// This will fail with "UNIQUE constraint failed"
await db.execute('CREATE (u:User {email: "alice@example.com"})');

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

Keywords

leangraph

FAQs

Package last updated on 09 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts