Neo4j
Neo4j integration is runtime-first.
Use @farming-labs/orm-neo4j when:
- the app already owns a real Neo4j driver or session
- a shared package wants to keep one storage layer across Neo4j, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Firestore, DynamoDB, Redis, or MongoDB-style runtimes
- you want one schema definition and one query surface while still letting the app keep Neo4j underneath
Supported Neo4j runtime inputs
- a
neo4j-driverDriver - a
neo4j-driverSession
The current runtime uses those official driver/session shapes directly. It does not invent another graph client layer.
Runtime setup
import neo4j from "neo4j-driver";
import { createOrm } from "@farming-labs/orm";
import { createNeo4jDriver } from "@farming-labs/orm-neo4j";
import { appSchema } from "./schema";
const driver = neo4j.driver(
process.env.NEO4J_URI!,
neo4j.auth.basic(process.env.NEO4J_USERNAME!, process.env.NEO4J_PASSWORD!),
);
const orm = createOrm({
schema: appSchema,
driver: createNeo4jDriver({
client: driver,
database: "neo4j",
}),
});From there, shared code keeps using the same unified API:
const user = await orm.user.findUnique({
where: {
email: "ada@farminglabs.dev",
},
select: {
id: true,
email: true,
sessions: {
select: {
token: true,
},
},
},
});What the Neo4j driver is doing
The Neo4j driver keeps the shared ORM surface and stores records through Neo4j-managed nodes plus ORM-owned lookup metadata.
It:
- accepts the app's real Neo4j driver or session
- executes writes through Neo4j transactions
- uses ORM-managed unique lookups and follow-up relation reads
- keeps the same normalized error and capability surface as the other runtimes
That means a package can write its storage layer once while each app decides whether the actual execution stack is Neo4j, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Firestore, DynamoDB, Redis, MongoDB, or Mongoose.
Runtime helper path
If a framework or shared package wants to accept the raw Neo4j client directly, use the runtime helpers:
import { createOrmFromRuntime } from "@farming-labs/orm-runtime";
const orm = await createOrmFromRuntime({
schema: appSchema,
client: driver,
});You can also pass a Neo4j session directly:
const session = driver.session({ database: "neo4j" });
const orm = await createOrmFromRuntime({
schema: appSchema,
client: session,
});That is the cleanest path for higher-level integrations that do not want to branch on Neo4j specifically.
Setup helpers
The setup helpers work with Neo4j too:
import { bootstrapDatabase, pushSchema } from "@farming-labs/orm-runtime/setup";
await pushSchema({
schema: appSchema,
client: driver,
});
const orm = await bootstrapDatabase({
schema: appSchema,
client: driver,
});For Neo4j runtimes, that setup path creates the ORM-owned constraints and indexes needed for record and unique-lock storage.
That is useful when a package or framework wants:
- repeatable local or CI bootstrap
- one setup path across runtime families
- no separate Neo4j-only setup API at the package boundary
Relation support
The Neo4j runtime is graph-backed, but the current ORM layer still keeps relation loading conservative:
belongsTohasOnehasMany- explicit join-table
manyToMany
Those reads work through the shared relation resolver and follow-up lookups. This runtime is not trying to expose a Cypher-native graph traversal planner through the ORM API.
That makes it a strong fit for:
- auth and account graphs
- org/member/workspace graphs
- connected app/platform state
while still preserving the same shared query surface as the rest of the repo.
Transactions and mutations
Neo4j write transactions map into the unified ORM transaction surface:
await orm.transaction(async (tx) => {
const user = await tx.user.create({
data: {
email: "ada@farminglabs.dev",
name: "Ada",
},
select: {
id: true,
},
});
await tx.session.upsert({
where: {
token: "session-token",
},
create: {
userId: user.id,
token: "session-token",
expiresAt: new Date("2027-01-01T00:00:00.000Z"),
},
update: {
expiresAt: new Date("2027-01-01T00:00:00.000Z"),
},
});
});The same runtime also supports:
createcreateManyupdateupdateManyupsertdeletedeleteMany- compound-unique lookups
- model-level constraint enforcement
Verification
Run the fast local Neo4j suite with:
pnpm test:local:neo4jThat package-local suite uses a Neo4j-shaped harness so you can verify the runtime, setup helpers, and transaction behavior without running a local Neo4j service first.
If you want a real Bolt-backed run against your own local or shared Neo4j instance, use:
export FARM_ORM_LOCAL_NEO4J_URI=bolt://127.0.0.1:7687
export FARM_ORM_LOCAL_NEO4J_USERNAME=neo4j
export FARM_ORM_LOCAL_NEO4J_PASSWORD=your-password
pnpm test:neo4j:realImportant boundaries
- generated integer IDs are not supported
- schema-qualified table namespaces are not supported
- this runtime is graph-backed, but it is not a Cypher-native graph query builder
- relation loading stays conservative instead of claiming native graph traversal planning
Why it fits well
Neo4j already gives apps a strong graph runtime.
Farming Labs ORM sits one layer above that:
- app code keeps Neo4j
- package code keeps one schema and one storage layer
- runtime helpers can accept the raw Neo4j driver or session
- setup helpers can bootstrap the ORM-owned constraints and indexes
That is the main value: Neo4j apps can participate in the same package-level storage contract as Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Firestore, DynamoDB, Redis, MongoDB, and Mongoose apps.
How is this guide?