Runtime
In Farming Labs ORM, runtime is the layer that executes the typed query API.
It is separate from:
- schema authoring
- code generation
The shape
import { createMemoryDriver, createOrm } from "@farming-labs/orm";
import { authSchema } from "./schema";
const orm = createOrm({
schema: authSchema,
driver: createMemoryDriver(),
});That gives you typed model clients such as orm.user, orm.session, and
orm.account.
It also exposes the attached runtime handle on orm.$driver. That gives app
code access to the underlying instance that was passed into the driver:
const orm = createOrm({
schema: authSchema,
driver: createPrismaDriver({ client: prisma }),
});
orm.$driver.kind; // "prisma"
orm.$driver.client; // the same PrismaClient instance
orm.$driver.capabilities.upsert; // "native" | "emulated" | "none"
orm.$driver.capabilities.returning.update; // whether update returns records in this ORM layerThe handle also exposes read-only capability metadata:
const orm = createOrm({
schema: authSchema,
driver: createPgPoolDriver(pool),
});
orm.$driver.capabilities.supportsJSON; // true
orm.$driver.capabilities.supportsTransactions; // true
orm.$driver.capabilities.nativeRelationLoading; // "partial"
orm.$driver.capabilities.numericIds; // "none" | "manual" | "generated"
orm.$driver.capabilities.textMatching.equality; // "database-default" | "case-sensitive" | "case-insensitive"
orm.$driver.capabilities.nativeRelations.hasMany; // true | falseThat metadata is derived from the driver, not passed in manually through
createOrm(...). It is frozen read-only runtime metadata, so higher layers can
inspect it safely without treating it as mutable app state.
The repo also verifies this against real local database-backed runtimes. The
local SQL and Prisma integration suites assert the same capability metadata on
orm.$driver and again inside real transaction scopes.
If you need to inspect a raw client before building a driver, the core package
also exposes detectDatabaseRuntime(...):
import { detectDatabaseRuntime } from "@farming-labs/orm";
const detected = detectDatabaseRuntime(prisma);
detected?.kind; // "prisma"
detected?.dialect; // "postgres" | "mysql" | "sqlite" when detectable
detected?.source; // "client" | "db" | "connection" | "pool" | "database"
detected?.client; // the same instance you passed inIf detection fails, use the diagnostic report instead of guessing:
import { inspectDatabaseRuntime } from "@farming-labs/orm";
const report = inspectDatabaseRuntime(client);
report.runtime; // detected runtime or null
report.summary; // human-readable summary
report.hint; // recommended next step or override
report.candidates; // heuristics that almost matchedIf you want the same fallback behavior in one step, use
@farming-labs/orm-runtime:
import { createOrmFromRuntime } from "@farming-labs/orm-runtime";
const orm = await createOrmFromRuntime({
schema: authSchema,
client: prisma,
});
orm.$driver.kind; // "prisma"
orm.$driver.client; // the same PrismaClient instanceThat light root entrypoint keeps createOrm(...) itself explicit while giving
higher-level integrations a clean “accept the raw client and normalize it”
entrypoint.
The setup helpers now live on the Node-only subpath:
pushSchema(...)applySchema(...)bootstrapDatabase(...)
Import them from @farming-labs/orm-runtime/setup. They use the same runtime
detection layer to prepare direct SQL, Prisma, Drizzle, Kysely, MikroORM,
TypeORM, Sequelize, EdgeDB / Gel, Neo4j, Cloudflare D1, Cloudflare KV, Redis,
Supabase, MongoDB, Mongoose, Firestore, DynamoDB, and Unstorage runtimes
without inventing a separate config surface.
There is also a dedicated guide for this flow:
For EdgeDB / Gel specifically, the runtime path is supported directly through the Gel SQL client, while schema management stays in the app's own Gel migration or SQL workflow.
The runtime also understands declared model-level compound unique keys. If a
schema says account(provider, accountId) is unique, live drivers can use that
same shape in findUnique(...) and upsert(...).
Numeric IDs are first-class now too. id({ type: "integer" }) gives you manual
numeric IDs, while id({ type: "integer", generated: "increment" }) enables
auto-generated numeric IDs on the supporting SQL-family, Prisma, and memory
runtimes.
Query surface
The runtime client supports:
findUniquefindOnefindFirstfindManycountcreatecreateManyupdateupdateManyupsertdeletedeleteManytransactionbatch
Relations
All live runtime drivers support:
belongsTohasOnehasMany- explicit join-table
manyToMany
That means nested queries like user.profile, user.sessions, and
session.user work across the supported runtimes.
For loading strategy, the current runtime behaves like this:
- direct SQL, Drizzle, and Kysely use a native single-query path for:
- singular relation chains built from
belongsTo(...)andhasOne(...) - simple
hasMany(...)and explicit join-tablemanyToMany(...)branches when the relation branch does not add its ownwhere,orderBy,take, orskip
- singular relation chains built from
- Prisma translates supported nested relation branches directly into Prisma
delegate
selecttrees, including simple explicit join-tablemanyToMany(...)convenience traversal through the generated join model - relation branches that add their own filtering, ordering, paging, or other unsupported planner requirements still fall back to the shared relation resolver
- MongoDB and Mongoose keep using the relation resolver today
That fallback behavior is mostly a current implementation boundary, not a statement that the backend cannot do more. PostgreSQL, MySQL, SQLite, Prisma, and MongoDB all have richer native relation-loading options available. This repo currently uses the native path for the safest direct branches first and keeps the shared resolver for the more complex shapes until those planners are added.
It also means unique lookups can be:
- a generated
id - a single
.unique()field such asemailortoken - a declared compound unique constraint such as
provider + accountId
Capabilities and Errors
Every live ORM client exposes read-only capability metadata on orm.$driver.capabilities.
That includes:
- basic value support like JSON, dates, booleans, and transactions
- numeric ID support
- namespace and transactional DDL support
- mutation-returning support
- upsert behavior
- text comparison behavior
- relation-loading capability
Runtime failures are normalized too. Duplicate-key, foreign-key, missing-table,
deadlock, and transaction-conflict failures are exposed as OrmError values
through the unified ORM client, so higher-level integrations do not need to
parse Prisma, SQL, EdgeDB, MongoDB, or Mongoose error formats separately.
Runtime-aware schema setup failures from @farming-labs/orm-runtime/setup are
exposed separately as RuntimeSetupError, so bootstrap and push failures keep
the stage, runtime kind, dialect, and underlying cause available.
Available drivers
createMemoryDriver(...)from@farming-labs/ormcreatePrismaDriver(...)from@farming-labs/orm-prismacreateDrizzleDriver(...)from@farming-labs/orm-drizzlecreateKyselyDriver(...)from@farming-labs/orm-kyselycreateMikroormDriver(...)from@farming-labs/orm-mikroormcreateTypeormDriver(...)from@farming-labs/orm-typeormcreateSequelizeDriver(...)from@farming-labs/orm-sequelizecreateEdgeDbDriver(...)from@farming-labs/orm-edgedbcreateD1Driver(...)from@farming-labs/orm-d1createKvDriver(...)from@farming-labs/orm-kvcreateRedisDriver(...)from@farming-labs/orm-rediscreateSupabaseDriver(...)from@farming-labs/orm-supabasecreateSqliteDriver(...)from@farming-labs/orm-sqlcreateMysqlDriver(...)from@farming-labs/orm-sqlcreatePgPoolDriver(...)from@farming-labs/orm-sqlcreatePgClientDriver(...)from@farming-labs/orm-sqlcreateSupabasePoolDriver(...)from@farming-labs/orm-sqlcreateSupabaseClientDriver(...)from@farming-labs/orm-sqlcreateFirestoreDriver(...)from@farming-labs/orm-firestorecreateMongoDriver(...)from@farming-labs/orm-mongocreateMongooseDriver(...)from@farming-labs/orm-mongoosecreateDynamodbDriver(...)from@farming-labs/orm-dynamodbcreateUnstorageDriver(...)from@farming-labs/orm-unstoragecreateOrmFromRuntime(...)from@farming-labs/orm-runtimecreateDriverFromRuntime(...)from@farming-labs/orm-runtimepushSchema(...)from@farming-labs/orm-runtime/setupapplySchema(...)from@farming-labs/orm-runtime/setupbootstrapDatabase(...)from@farming-labs/orm-runtime/setup
Why this matters
Libraries can write one storage layer against createOrm(...) and let each app
bring its own runtime.
async function findUserByEmail(orm: typeof authOrm, email: string) {
return orm.user.findUnique({
where: { email },
select: {
id: true,
email: true,
sessions: {
select: { token: true },
},
},
});
}That helper stays the same whether the app uses Prisma, Drizzle, TypeORM, Sequelize, EdgeDB / Gel, Cloudflare D1, Cloudflare KV, Redis, Supabase, direct SQL, Firestore, DynamoDB, Unstorage, MongoDB, or Mongoose.
The same is true for compound auth lookups:
async function findAccount(orm: typeof authOrm, provider: string, accountId: string) {
return orm.account.findUnique({
where: {
provider,
accountId,
},
});
}Local verification
pnpm test already includes these real integration suites. Use the commands
below when you want to rerun the database-backed paths directly.
pnpm test:local
pnpm test:local:prisma
pnpm test:local:drizzle
pnpm test:local:sqlite
pnpm test:local:postgres
pnpm test:local:mysql
pnpm test:local:supabase
pnpm test:local:dynamodb
pnpm test:local:unstorage
pnpm test:local:mongodbContinue
How is this guide?