# Documentation ## Overview URL: /docs Write your storage layer once and let Farming Labs ORM handle the translation across databases and ORM stacks. Overview Farming Labs ORM lets you write your storage layer once, then handles the translation across databases and ORM stacks underneath. Library authors and framework authors can keep one schema definition and one storage-facing API while letting each app choose how that gets generated or executed. The model is simple: 1. Define a schema in @farming-labs/orm 2. Generate Prisma, Drizzle, or SQL artifacts with @farming-labs/orm-cli 3. Run the same typed API through a runtime driver In practice, that means: - one storage layer for shared packages - one typed ORM and query surface for runtime code - one schema definition that can translate across databases and ORM stacks - one generator CLI for app-side artifacts Start here 1. Getting started 2. Unified schema 3. Runtime 4. Integrations 5. Support matrix 6. Changelogs 7. Use cases --- ## CLI Generation URL: /docs/cli How the CLI loads schemas and writes Prisma, Drizzle, and SQL output. CLI Generation @farming-labs/orm-cli reads schema objects exported by @farming-labs/orm. It does not invent a second schema language. Config shape How schema merging works The CLI merges all schemas from config.schemas into one combined schema before rendering targets. Important rule: - duplicate model names are rejected That keeps package composition explicit and prevents silent overwrites. Core commands generate generate renders output to disk. Prisma Supported options: - provider: "postgresql" | "mysql" | "sqlite" - datasourceName - generatorName - out - mode: "block" | "replace" Drizzle Supported options: - dialect: "pg" | "mysql" | "sqlite" - out SQL Supported options: - dialect: "postgres" | "mysql" | "sqlite" - out check check renders the same target in memory and compares it to the file already on disk. That makes it useful for: - CI validation - generator drift checks - confirming committed artifacts are still current Render as a string If you want the generated output in memory instead of writing it to disk, use the render helpers from @farming-labs/orm directly: Those helpers return strings: - renderPrismaSchema(...) -> schema.prisma contents - renderDrizzleSchema(...) -> generated Drizzle TypeScript source - renderSafeSql(...) -> generated SQL DDL This is useful when you want to: - inspect generated output in tests - embed generated schema text in another tool - preview generated content without writing a file If you are using multiple schemas through the CLI config, the CLI merges them before writing files. The public string helpers work directly on a schema object, so multi-schema rendering still needs a merged schema first. Prisma block mode vs replace mode mode: "replace" Writes the generated Prisma file as the whole file contents. mode: "block" If the file already exists, the CLI inserts or updates a generated block: That is useful when the application wants to keep non-generated Prisma content around the generated schema region. What generators actually read The generators consume normalized manifest data from the schema: - table names - field names - mapped columns - scalar kinds - nullability - uniqueness - default/generated values - foreign-key references They also consume direct relation metadata for: - belongsTo(...) - hasOne(...) - hasMany(...) Explicit join-table manyToMany(...) stays explicit in generated Prisma and Drizzle output through the join model instead of becoming an implicit shortcut. Target examples Prisma output From: the Prisma generator will emit: Drizzle output For PostgreSQL, a simple user model becomes output shaped like: Safe SQL output The SQL generator writes forward-safe DDL like: What the CLI intentionally does not do - It does not maintain its own schema DSL - It does not silently merge duplicate model names - It does not execute generated Prisma or Drizzle code for you - It does not replace a full migration framework by itself beyond the safe SQL generation path Release and publish flow The repo’s package release flow is bumpp-driven through a checked-in bump.config.ts: For prereleases: Useful safety commands: The bump.config.ts file is the source of truth for the release files, commit message, and git tag shape. release:latest runs that bump flow and then publishes the new version to npm. publish:* is still useful when you need to retry a publish without bumping again. Where each target fits - Prisma integration - Drizzle integration - SQL databases Design constraint The CLI depends on @farming-labs/orm directly, so schema semantics only exist in one place. That is one of the most important architectural rules in the repo. --- ## Getting Started URL: /docs/getting-started Define a schema, add a config file, and generate your first artifacts. Getting Started The shortest path is: 1. Define a schema in @farming-labs/orm 2. Add farm-orm.config.ts 3. Generate target output 4. Pick a runtime driver 1. Define a schema 2. Add farm-orm.config.ts 3. Generate artifacts Generated Prisma and Drizzle output already includes direct relation metadata for: - belongsTo - hasOne - hasMany Join-table manyToMany stays explicit through the join model. The same schema can also declare compound uniques and indexes. In the example above, account(provider, accountId) becomes: - a generated unique constraint in Prisma / Drizzle / SQL - a runtime-safe findUnique(...) and upsert(...) key across supported drivers 4. Pick a runtime Memory Prisma Drizzle Direct SQL EdgeDB / Gel Xata Cloudflare D1 Cloudflare KV Redis / Upstash Redis Supabase JS TypeORM MikroORM Sequelize MongoDB DynamoDB Unstorage Mongoose Or detect the runtime automatically If you already have the raw runtime instance and want the helper to build the driver for you, use @farming-labs/orm-runtime: That path is especially useful for higher-level integrations that want to accept a raw Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, SQL, EdgeDB / Gel, Cloudflare D1, Cloudflare KV, Redis, Supabase, Firestore, DynamoDB, Unstorage, MongoDB, or Mongoose runtime. 5. Use the client 6. Verify with local databases pnpm test already runs the real integration matrix. These commands are useful when you want to rerun the database-backed suites directly. Continue - Runtime - Prisma integration - Drizzle integration - Sequelize integration - TypeORM integration - MikroORM integration - SQL databases - Cloudflare KV - Redis / Upstash Redis - Supabase - DynamoDB - Unstorage - MongoDB --- ## Integrations URL: /docs/integrations How Farming Labs ORM fits into Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, EdgeDB / Gel, Neo4j, SurrealDB, Cloudflare D1, Cloudflare KV, Redis / Upstash, SQL-first, Supabase, Xata, Firestore, DynamoDB, Unstorage, and MongoDB-oriented workflows. Integrations Farming Labs ORM lets a package or app write its storage layer once, then run it through the actual database or ORM stack the app already owns. That usually means: 1. define one schema with @farming-labs/orm 2. generate app-facing artifacts when a stack wants them 3. run the unified query/runtime layer against the real client the app passes in So instead of building one adapter for Prisma, another for Drizzle, another for Kysely, another for MongoDB, and another for Firestore, the package can keep one storage contract and let the runtime layer handle the translation. If you want the exact support answer, use the Support Matrix. This page is the simpler overview. Integration families Generation-first SQL stacks Use these when the app wants generated artifacts: - Prisma generation - Drizzle generation - safe SQL generation These are the right fit when the app already wants schema.prisma, generated Drizzle tables, or plain SQL DDL files as part of its own workflow. Runtime-first SQL and graph stacks Use these when the app already owns a live relational client: - Prisma - Drizzle - Kysely - MikroORM - TypeORM - Sequelize - EdgeDB / Gel - Neo4j - SurrealDB - Cloudflare D1 - Xata - direct SQL clients This is the path for package authors who want to accept a raw client and keep the higher-level storage logic unchanged. PostgreSQL platforms Hosted PostgreSQL platforms such as Supabase can fit through either the raw PostgreSQL layer or the direct Supabase JS client runtime. That means the same schema can still power: - generated Prisma / Drizzle / SQL output - live ORM queries through a PostgreSQL client or a Supabase client - setup helpers when the app chooses the PostgreSQL path Document and key-value runtimes Farming Labs ORM also supports document and key-value style runtimes when the app is not SQL-first: - MongoDB - Mongoose - Cloudflare KV - Redis / Upstash Redis - Firestore - DynamoDB - Unstorage These runtimes still keep the same schema and query API shape, but they rely more heavily on fallback relation loading and ORM-managed lookup behavior rather than native SQL joins. Two important boundaries: - Firestore, Cloudflare KV, Redis, and Unstorage do not have SQL-style schema push, so setup helpers are intentionally no-op there - Unstorage is useful for lightweight records, sessions, tokens, and package-owned state, but it is not the recommended runtime for highly relational or join-heavy workloads What “supported” means When a runtime is supported here, the unified ORM API already translates into real calls for that stack. Examples: - @farming-labs/orm-prisma translates into Prisma delegate calls - @farming-labs/orm-drizzle translates into Drizzle-backed queries - @farming-labs/orm-kysely translates into Kysely-backed queries - @farming-labs/orm-mikroorm translates into MikroORM-backed relational operations - @farming-labs/orm-sql translates into direct SQL operations - @farming-labs/orm-typeorm translates into TypeORM-backed relational operations - @farming-labs/orm-sequelize translates into Sequelize-backed relational operations - @farming-labs/orm-edgedb translates into Gel SQL-backed relational operations - @farming-labs/orm-neo4j translates into Neo4j-backed graph/document operations through the shared ORM surface - @farming-labs/orm-surrealdb translates into SurrealDB-backed multi-model operations through the official client - @farming-labs/orm-d1 translates into Cloudflare D1 binding operations - @farming-labs/orm-kv translates into Cloudflare KV namespace operations - @farming-labs/orm-redis translates into Redis and Upstash-compatible key-value operations - @farming-labs/orm-supabase translates into direct Supabase table API operations - @farming-labs/orm-xata translates into Xata-backed SQL operations through the official client - @farming-labs/orm-firestore translates into Firestore document operations - @farming-labs/orm-dynamodb translates into DynamoDB document / key-value operations - @farming-labs/orm-unstorage translates into Unstorage-backed key-value operations - @farming-labs/orm-mongo and @farming-labs/orm-mongoose translate into MongoDB-oriented document operations Guides Prisma integration · Drizzle integration · Kysely integration · MikroORM integration · TypeORM integration · Sequelize integration · SQL integration · EdgeDB integration · Neo4j integration · SurrealDB integration · Cloudflare D1 integration · Cloudflare KV integration · Redis integration · Supabase integration · Xata integration · Firestore integration · DynamoDB integration · Unstorage integration · MongoDB integration --- ## Cloudflare D1 URL: /docs/integrations/cloudflare-d1 How to use Farming Labs ORM with Cloudflare D1 through a worker-native runtime. Cloudflare D1 Cloudflare D1 support in Farming Labs ORM is the worker-native SQLite answer for teams that want one storage layer without introducing a separate Cloudflare-only adapter surface. The supported path is the real D1 binding: - D1Database - D1DatabaseSession That keeps the runtime Cloudflare-friendly while still letting package and framework code stay backend-agnostic. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into D1-backed SQLite operations with: - one schema definition - one query API - one runtime helper path - one capability surface - one normalized error surface That is a strong fit for framework modules, auth state, billing state, cache metadata, and other shared storage contracts running inside Workers. Create the runtime directly Use the runtime helper path If a framework or shared package wants to accept the raw D1 binding and normalize it later, use the runtime helpers: That keeps the package boundary generic instead of forcing a D1-specific branch. Setup helpers The D1 runtime itself is Worker-friendly. The setup helpers are different: - createD1Driver(...) works inside the Worker runtime - createOrmFromRuntime(...) works inside the Worker runtime - pushSchema(...), applySchema(...), and bootstrapDatabase(...) are still best used in local, CI, or other Node-managed bootstrap flows because @farming-labs/orm-runtime/setup stays Node-only That means the common pattern is: - use the D1 runtime directly inside Workers - use setup helpers in local or CI bootstrap flows when you want repeatable schema prep Joins, relations, and transactions D1 is still SQLite-backed, so the relational query surface works well. That means: - nested relation selections work - compound unique lookups and upserts work - generated integer ids work - the shared SQL relation planner can still use native query shapes where supported At the same time, the runtime stays conservative about transaction semantics: - orm.transaction(...) is available - orm.$driver.capabilities.supportsTransactions is false - the D1 runtime does not claim the same long-lived interactive rollback semantics as a traditional Node SQL connection That keeps the capability story honest for framework and auth integrations. What is supported well - string ids - generated integer ids - integer() - json() - enumeration() - decimal() - relation selections - compound unique lookups and upserts - raw runtime detection - createOrmFromRuntime(...) Important limits - schema-qualified table names are not supported - setup helpers are for local, CI, or other Node-managed flows, not direct Worker-request imports - orm.transaction(...) should not be treated as a promise of full interactive rollback semantics in this runtime - very large bigint() values beyond the JavaScript safe integer range are not precision-preserving in this runtime Why it matters This keeps Cloudflare D1 inside the same bigger ORM story: - write your storage layer once - keep one schema definition - let the app choose D1 - avoid owning a separate Cloudflare-only adapter surface --- ## Cloudflare KV URL: /docs/integrations/cloudflare-kv How to use Farming Labs ORM with Cloudflare KV through a worker-native key-value runtime. Cloudflare KV Cloudflare KV support in Farming Labs ORM is the worker-native key-value path for teams that want one storage layer without building a separate Cloudflare-only adapter surface. The supported path is the real Worker binding: - KVNamespace - a local Miniflare KV namespace That keeps the runtime Cloudflare-friendly while still letting framework and package code stay backend-agnostic. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into KV-backed operations with: - one schema definition - one query API - one runtime helper path - one capability surface - one normalized error surface That is a strong fit for sessions, tokens, cache metadata, independent key-value state, rate limits, and other lightweight framework-owned records inside Workers. Create the runtime directly Use the runtime helper path If a framework or shared package wants to accept the raw KV namespace and normalize it later, use the runtime helpers: That keeps the package boundary generic instead of forcing a Cloudflare KV specific branch. Setup helpers Cloudflare KV is a runtime-first backend, not a schema-push backend. That means: - createKvDriver(...) works directly against the namespace - createOrmFromRuntime(...) works directly against the namespace - pushSchema(...) and applySchema(...) are intentional no-ops - bootstrapDatabase(...) still works as the single convenience entrypoint Joins, relations, and transactions Cloudflare KV is not a join-first runtime, so the ORM stays explicit and conservative. That means: - nested relation selections still work through follow-up reads - unique and compound-unique lookups are ORM-managed - orm.transaction(...) exists, but the runtime does not claim SQL-style transaction semantics - generated integer IDs are not supported What is supported well - string IDs - manual numeric IDs - integer() - json() - enumeration() - decimal() - relation selections through follow-up reads - compound unique lookups and upserts - raw runtime detection - createOrmFromRuntime(...) Important limits - schema-qualified table names are not supported - pushSchema(...) and applySchema(...) are no-ops - the Cloudflare KV runtime is not the recommended fit for highly relational or join-heavy workloads - uniqueness is ORM-managed rather than backed by a native transaction surface Why it matters This keeps Cloudflare KV inside the same bigger ORM story: - write your storage layer once - keep one schema definition - let the app choose Cloudflare KV - avoid owning a separate Cloudflare-only adapter surface --- ## Drizzle URL: /docs/integrations/drizzle How the unified schema and runtime fit into Drizzle-based apps. Drizzle Drizzle integration in this repo is now both: - generation-first when you want generated Drizzle schema files - runtime-capable when the app already has a Drizzle database instance Basic config Supported Drizzle dialects - pg - mysql - sqlite Runtime translation status The repo now ships a live @farming-labs/orm-drizzle runtime package. That means the unified query API can already run through Drizzle-backed apps for: - PostgreSQL - MySQL - SQLite The current runtime package unwraps the underlying client from the Drizzle database handle and routes the unified API through the existing SQL runtime contract. From a library author point of view, that still means one stable API: That also means Drizzle inherits the SQL runtime's native relation-loading path. Reads such as session.user, session.user.profile, user.sessions, and simple explicit join-table manyToMany(...) branches can now load through one SQL statement. Relation branches still fall back when they add extra relation-level filtering, ordering, or paging. That split is about the current Farming ORM planner, not a hard limit in Drizzle-backed SQL apps. As the SQL runtime grows richer native plans, Drizzle inherits them automatically. That live Drizzle matrix now also verifies: - integer() fields with comparison filters - json() fields with raw JSON equality filters - compound-unique lookups and upserts - JSON mutation round-trips Runtime example Example generated output From: the generator emits output shaped like: If you want the generated Drizzle file as a string instead of writing it to disk, use renderDrizzleSchema(...) from @farming-labs/orm. The CLI page has the in-memory example: CLI generation. For direct relations, the generator also emits relations(...) helpers for: - belongsTo(...) - hasOne(...) - hasMany(...) Join-table-backed manyToMany(...) is still represented through the explicit join model rather than a direct Drizzle relation shortcut. How it fits into a Drizzle app Typical flow: 1. define the reusable schema once 2. either run farm-orm generate drizzle for generated schema files 3. or pass the live Drizzle database to createDrizzleDriver(...) 4. keep writing one library-facing query API either way That means Farming Labs ORM can either feed the app-side Drizzle stack through generation, or sit above it as a runtime translation layer. Why Drizzle teams might like this - generated table definitions from shared package schemas - one unified runtime API for shared packages even when the app already uses Drizzle - no need to manually rewrite the same package tables per app - easier multi-package monorepo composition Important nuance The Drizzle generator now covers the direct relation helpers above, and the live runtime already covers: - belongsTo(...) - hasOne(...) - hasMany(...) - explicit join-table manyToMany(...) Generated Drizzle output still keeps many-to-many relationships explicit through the join model, which matches how Drizzle teams usually model those tables anyway. Local verification The repo verifies Drizzle locally against PostgreSQL, MySQL, and SQLite. Run it with: Helpful references Drizzle fundamentals · Drizzle relation docs · Drizzle transaction docs --- ## DynamoDB URL: /docs/integrations/dynamodb How to use Farming Labs ORM with AWS DynamoDB through one unified key-value/document runtime. DynamoDB DynamoDB support in Farming Labs ORM is the key-value/document-runtime answer for teams that want one storage layer without owning a separate AWS-only adapter surface. The supported path is raw AWS SDK clients: - DynamoDBClient - DynamoDBDocumentClient That makes it a good fit for shared libraries and framework-owned storage layers, including auth-style integrations such as Better Auth or Auth.js, where the package wants one storage contract even though the backend is not SQL-first. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into DynamoDB operations with: - one schema definition - one query API - one runtime-helper path - one capability surface - one normalized error surface - one setup/bootstrap path for creating the model tables Create the runtime directly If your app already owns a DynamoDBDocumentClient, you can pass it as the document client override too: Use the runtime helper path If a framework or shared package wants to accept a raw DynamoDB client and normalize it later, use the runtime helpers instead: That keeps the package boundary generic instead of requiring a DynamoDB-only branch. Setup helpers The setup helpers work with DynamoDB too: For DynamoDB, pushSchema(...) creates one table per model with an internal partition key. The runtime stores both: - record items - internal unique-lock items inside that table, which lets exact unique and compound-unique lookups stay fast without making the consumer package invent a separate locking layer. Joins, relations, and transactions DynamoDB does not have native joins. That does not mean relation selections stop working. The ORM resolves relation branches through follow-up reads, so code like this still works: The runtime is similarly conservative about transactions: - orm.$driver.capabilities.supportsTransactions is false - orm.transaction(...) is available as a storage-layer boundary, but it is not advertised as full database transaction semantics - create, update, upsert, and delete flows still coordinate the necessary consecutive writes and unique checks so higher-level libraries do not need a DynamoDB-only branch That is the important integration story for frameworks like Better Auth: storage code can stay unified even when the backend does not offer SQL joins or SQL-style transactions. What is supported well - string ids - manual numeric ids - integer() - json() - enumeration() - bigint() - decimal() - compound unique lookups and upserts - relation selections through follow-up queries - raw client detection - createOrmFromRuntime(...) - pushSchema(...) and bootstrapDatabase(...) - normalized duplicate-key and missing-table errors Important limits - generated integer ids are not supported on DynamoDB - schema-qualified table names are not supported - relation loading is fallback-based, not native-join based - orm.transaction(...) should not be treated as cross-write atomic database transaction semantics in this runtime - non-unique filtering is scan-based, so very large-table access patterns should prefer id, unique, or compound-unique lookups when possible Local verification The repo verifies the DynamoDB runtime locally through a Dynalite-backed integration suite. Run it with: Why it matters This keeps DynamoDB inside the same bigger ORM story: - write your storage layer once - keep one schema definition - let the app choose DynamoDB - avoid owning a separate DynamoDB-only adapter surface --- ## EdgeDB / Gel URL: /docs/integrations/edgedb How to use Farming Labs ORM with the official Gel SQL client as a runtime-only relational path. EdgeDB / Gel Farming Labs ORM supports EdgeDB through the current Gel SQL client runtime. That means the supported path is: - the official gel JavaScript client - its querySQL(...), executeSQL(...), and transaction(...) surface - one unified ORM/runtime layer on top of the existing Gel database This is intentionally a runtime-first integration, not a Gel SDL or schema management replacement. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into Gel SQL-backed relational queries with: - one schema definition - one query API - one runtime helper path - one capability surface - one normalized error surface That makes it useful for frameworks, reusable platform modules, auth state, billing state, internal tools, and other storage layers that should not fork into one implementation per backend. Create the runtime directly Use the runtime helper path If a framework or shared package wants to accept the raw Gel client and normalize it later, use the runtime helpers: That keeps the package boundary generic instead of forcing an EdgeDB-only branch. Setup helpers EdgeDB support here is runtime-first. That means: - createEdgeDbDriver(...) works against the Gel SQL client - createOrmFromRuntime(...) detects the Gel SQL client directly - pushSchema(...) and applySchema(...) intentionally do not try to manage Gel schemas or migrations - bootstrapDatabase(...) still works as the convenience entrypoint when the schema already exists So the common pattern is: - manage schema through the app's Gel migration / SDL / SQL workflow - use Farming Labs ORM as the runtime/query layer on top of that database Joins, relations, and transactions This runtime uses the PostgreSQL SQL dialect path underneath. That means: - nested relation selections work - compound unique lookups and upserts work - generated integer IDs work - transactions are supported - schema-qualified table names work in the runtime query path The main boundary is not query power. It is schema management: - this integration does not replace Gel's own schema and migration story - @farming-labs/orm-runtime/setup does not attempt to push or apply EdgeDB schemas for you What is supported well - string IDs - generated integer IDs - integer() - json() - enumeration() - decimal() - relation selections - compound unique lookups and upserts - raw runtime detection - createOrmFromRuntime(...) Important limits - this is a runtime bridge, not a Gel SDL generator - pushSchema(...) and applySchema(...) are intentional no-ops for this runtime - schema management should stay in the app's Gel migration or SQL workflow Why it matters This keeps EdgeDB / Gel inside the same bigger ORM story: - write your storage layer once - keep one schema definition for the shared ORM layer - let the app choose Gel as the live backend - avoid owning a separate EdgeDB-only storage implementation --- ## Firestore URL: /docs/integrations/firestore How to use Farming Labs ORM with Firebase / Firestore through one unified document runtime. Firestore Firestore support in Farming Labs ORM is the document-runtime answer for teams that want one storage layer without owning a separate Firebase-only adapter stack. The supported path is server-side Firestore clients, such as: - @google-cloud/firestore - firebase-admin/firestore This is not the browser SDK path. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into Firestore document operations: - one schema definition - one query API - one capability surface - one normalized error surface - one runtime-helper path That means a framework, auth package, billing package, or internal platform can keep one storage implementation while the app decides to use Firestore. Create the runtime directly Use the runtime helper path If a library or framework wants to accept a raw Firestore client and normalize it later, use the runtime helpers instead: That is the cleanest path for package-owned integrations. Setup helpers Firestore does not have SQL-style table DDL, so the setup helpers are honest about that. - pushSchema(...) is a no-op - applySchema(...) is a no-op - bootstrapDatabase(...) still works and returns the ORM client after that no-op setup stage Live verification The repo also includes an opt-in live Firestore check for emulator or cloud setups. Run it with: Keep those secrets out of git and CI logs. For CI, prefer encrypted repository secrets rather than checked-in files. If you want the real Firestore suite to run as part of the normal workspace test flow, export those same env vars before pnpm test: Joins and relations Firestore does not have native joins. That does not mean relation selections stop working. The ORM resolves relation branches through consecutive document queries, so code like this still works: That is the same replacement story libraries like NextAuth.js or Better Auth need: write the storage layer once, and let the ORM resolve the document-database differences underneath it. Transactions and constraints When the Firestore client exposes runTransaction(...), the Firestore runtime uses it for writes that need consistency: - create - createMany - update - updateMany - upsert - delete - deleteMany - explicit orm.transaction(...) That matters because model-level unique constraints and compound unique constraints are checked inside that write flow rather than being left as best-effort client-side checks. What is supported well - string ids - manual numeric ids - integer() - json() - enumeration() - bigint() - decimal() - one-to-one, one-to-many, and explicit join-table many-to-many relation resolution through ORM follow-up queries - compound unique lookups and upserts - normalized duplicate-key error handling Important limits - generated integer ids are not supported on Firestore - schema-qualified table names are not supported - relation loading is fallback-based, not native-join based - advanced filter shapes are still more limited than SQL-first runtimes - browser SDK clients are not the target here Why this matters This keeps Firestore inside the same bigger ORM story: - write your storage layer once - keep one schema definition - let the app choose Firestore - avoid owning a separate Firebase-only adapter surface --- ## Kysely URL: /docs/integrations/kysely How the unified schema and runtime fit into Kysely-based apps. Kysely Kysely integration in this repo is runtime-first. Use @farming-labs/orm-kysely when: - the app already owns a real Kysely instance - a shared package wants to keep writing the unified Farming ORM API - you want SQLite, MySQL, or PostgreSQL without rewriting the package storage layer Supported Kysely dialects - postgres - mysql - sqlite Runtime setup From there, library code keeps using the unified API: What the Kysely driver is doing The Kysely runtime package does not invent a second ORM API. Instead it: 1. receives the unified Farming ORM query 2. executes through the app’s real Kysely database 3. reuses the shared SQL runtime behavior for relation loading, filtering, sorting, paging, and mutation semantics That shared SQL runtime now includes a native relation-loading path, so queries like session.user, session.user.profile, user.sessions, and simple explicit join-table manyToMany(...) traversal can load through one SQL statement. Relation branches with relation-level filters, paging, or ordering still fall back to the shared resolver. That does not mean Kysely or the underlying database is missing join support. It only means the shared SQL runtime currently translates the direct singular subset natively and leaves the more complex branches on the fallback path for now. That keeps the runtime behavior aligned with: - @farming-labs/orm-sql - @farming-labs/orm-drizzle - @farming-labs/orm-prisma Relation support The Kysely runtime covers: - belongsTo(...) - hasOne(...) - hasMany(...) - explicit join-table manyToMany(...) That means auth-style lookups such as: work the same way they do in the other live runtimes. Mutations and transactions The Kysely runtime also supports: - create - createMany - update - updateMany - upsert - delete - deleteMany - transaction Example: The same live Kysely matrix also verifies: - integer() fields with numeric comparison filters - json() fields with raw JSON equality filters - compound-unique lookups and upserts - JSON mutation round-trips Local verification The repo verifies Kysely locally against SQLite, PostgreSQL, and MySQL. Run it with: Helpful references Kysely fundamentals Kysely transaction docs How it fits into a Kysely app Typical flow: 1. the library defines a reusable schema in @farming-labs/orm 2. the app creates its own Kysely instance normally 3. the app passes that instance to createKyselyDriver(...) 4. the shared package keeps one storage/query layer for every supported stack That is the main value: Kysely apps can participate in the same package-level storage contract as Prisma, Drizzle, SQL, and Mongo-backed apps. --- ## MikroORM URL: /docs/integrations/mikroorm How the unified schema and runtime fit into MikroORM-based PostgreSQL and MySQL apps. MikroORM MikroORM integration is runtime-first. Use @farming-labs/orm-mikroorm when: - the app already owns a real MikroORM instance or EntityManager - a shared package wants to keep one storage layer across MikroORM, Prisma, Drizzle, Kysely, TypeORM, Sequelize, direct SQL, Firestore, DynamoDB, or MongoDB-style runtimes - you want one schema definition and one query surface while still letting the app use MikroORM underneath Supported MikroORM dialect families - postgresql - mysql / mariadb The current repo verifies the live matrix on PostgreSQL and MySQL. Runtime setup From there, shared code keeps using the same unified API: What the MikroORM driver is doing The MikroORM driver does not invent another schema system. It: 1. accepts the app's real MikroORM instance or EntityManager 2. executes through MikroORM connections and MikroORM transactions 3. reuses the shared SQL runtime semantics for filtering, mutations, relation loading, compound unique lookups, numeric IDs, namespaces, and normalized errors That means a package can write its storage layer once while each app decides whether the actual execution stack is MikroORM, Prisma, Drizzle, Kysely, TypeORM, Sequelize, direct SQL, Firestore, DynamoDB, MongoDB, or Mongoose. Runtime helper path If a framework or shared package wants to accept the raw MikroORM client directly, use the runtime helpers: You can also pass a forked EntityManager directly: That is the cleanest path for higher-level integrations that do not want to branch on MikroORM specifically. Setup helpers The setup helpers work with MikroORM too: For MikroORM runtimes, that setup path renders safe SQL from the Farming Labs schema and applies it through the live MikroORM connection. That is especially useful when a package or framework wants: - repeatable test setup - one bootstrap path across runtime families - no separate MikroORM-only schema-push API at the package boundary Relation support The MikroORM runtime inherits the current SQL-family relation behavior: - native single-query loading for supported singular chains - native single-query loading for simple hasMany(...) and explicit join-table manyToMany(...) branches without relation-level modifiers - shared fallback relation resolution for more complex relation branches that add their own where, orderBy, take, or skip That means auth-style and framework-style relation reads still work through the same unified API surface. Transactions and mutations MikroORM transactions map into the unified ORM transaction surface: The same runtime also supports: - create - createMany - update - updateMany - upsert - delete - deleteMany - compound-unique lookups - model-level constraint enforcement Local verification The repo verifies MikroORM locally against PostgreSQL and MySQL. Run it with: If you want to point the suite at your own local database URLs, use: You can also target a single MikroORM family while debugging: The PostgreSQL and MySQL paths create isolated temporary databases during the run and clean those databases up afterward. Why it fits well MikroORM already gives apps a strong relational runtime abstraction. Farming Labs ORM sits one layer above that: - app code keeps MikroORM - package code keeps one schema and one storage layer - runtime helpers can still accept the raw MikroORM instance or EntityManager - setup helpers can still bootstrap the live database That is the main value: MikroORM apps can participate in the same package-level storage contract as Prisma, Drizzle, Kysely, TypeORM, Sequelize, direct SQL, Firestore, DynamoDB, MongoDB, and Mongoose apps. --- ## MongoDB URL: /docs/integrations/mongodb How MongoDB fits into Farming Labs ORM through the native MongoDB and Mongoose runtime packages. MongoDB MongoDB support is live through two runtime packages: - @farming-labs/orm-mongo Use this if your app uses the native mongodb client - @farming-labs/orm-mongoose Use this if your app already has Mongoose models Native MongoDB example Mongoose example Field mapping MongoDB support still uses the schema as the source of truth, including mapped field names. Library code keeps using logical names like id and emailVerified. The runtime handles id, emailverified, and other mapped fields. Relations Both Mongo runtimes support: - belongsTo - hasOne - hasMany - manyToMany They load related documents through follow-up queries and return the same typed result shape as the other runtimes. That does not mean MongoDB is missing native join-like features forever. A future Mongo-native aggregation / $lookup path can still be added later. The current fallback behavior just means this repo has not translated those richer native plans yet. The live MongoDB suites also verify: - integer() fields - json() fields - compound-unique lookups and upserts - JSON update and reload behavior Transactions - @farming-labs/orm-mongo uses MongoDB sessions when client or startSession is provided - @farming-labs/orm-mongoose uses Mongoose sessions when connection or startSession is provided - if no session source is configured, transactions fall back to non-transactional execution Helpful references: MongoDB transaction guide Mongoose transaction guide Why there are two Mongo packages MongoDB is not a SQL backend with different syntax. It has its own query, relation, and transaction model. That is why the repo ships: - one native Mongo runtime - one Mongoose runtime Both use the same schema contract and typed API, but they plug into different MongoDB application stacks. Local verification The repo verifies both Mongo runtimes locally: - native MongoDB runtime: pnpm --filter @farming-labs/orm-mongo test:local - Mongoose runtime: pnpm --filter @farming-labs/orm-mongoose test:local Or run both with: --- ## Neo4j URL: /docs/integrations/neo4j How the unified schema and runtime fit into Neo4j-backed graph applications. 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-driver Driver - a neo4j-driver Session The current runtime uses those official driver/session shapes directly. It does not invent another graph client layer. Runtime setup From there, shared code keeps using the same unified API: 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: 1. accepts the app's real Neo4j driver or session 2. executes writes through Neo4j transactions 3. uses ORM-managed unique lookups and follow-up relation reads 4. 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: You can also pass a Neo4j session directly: 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: 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: - belongsTo - hasOne - hasMany - 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: The same runtime also supports: - create - createMany - update - updateMany - upsert - delete - deleteMany - compound-unique lookups - model-level constraint enforcement Verification Run the fast local Neo4j suite with: That 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: Important 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. --- ## Prisma URL: /docs/integrations/prisma How the unified schema generates Prisma schema output and how the live Prisma runtime driver fits into Prisma-based apps. Prisma Prisma support in this repo is now both: - generation, through farm-orm generate prisma - runtime translation, through @farming-labs/orm-prisma What Prisma support means today Today, Prisma support means: - your schema can generate schema.prisma - the consuming app can keep its normal Prisma workflow - the unified runtime API can execute through a real PrismaClient Runtime translation status The unified query methods now translate into Prisma delegate calls through @farming-labs/orm-prisma. That means code like this: can execute through PrismaClient without the library author rewriting the same storage helper for Prisma separately. Supported direct relation branches now translate into Prisma's native nested select tree instead of always bouncing back through the fallback resolver. That means reads like session.user and session.user.profile stay on the base Prisma delegate call. Simple explicit join-table manyToMany(...) convenience traversal now also stays on the base Prisma delegate call. The driver rewrites that logical branch through the generated join model instead of issuing follow-up runtime queries. When a query still falls back, that does not mean Prisma itself lacks relation loading support. It means the Farming ORM Prisma driver has not yet translated that logical relation shape natively. The remaining fallback cases are mostly relation branches that add their own filtering, ordering, paging, or other planner requirements beyond the currently translated subset. Runtime setup From there, the unified runtime methods execute through Prisma: Relation support The Prisma runtime package already covers: - belongsTo(...) - hasOne(...) - hasMany(...) - explicit join-table manyToMany(...) The important detail is that explicit join-table manyToMany(...) traversal is still modeled through the Farming ORM join table contract. The native Prisma path does not rely on Prisma implicit many-to-many shortcuts. It rewrites the logical convenience branch through the generated join model instead. That keeps the runtime behavior aligned with the same schema contract used by the SQL and Mongoose drivers. Optional model mapping If the logical model name and the Prisma delegate name do not match exactly, you can pass delegate overrides: Most apps that generate Prisma from the same Farming ORM schema do not need this. Basic generator config Supported Prisma providers - postgresql - mysql - sqlite The runtime driver talks to PrismaClient, so it follows Prisma’s provider surface for relational apps. In this repo, the real local integration coverage is currently exercised against SQLite, PostgreSQL, and MySQL. That live Prisma matrix now also covers: - compound-unique lookups and upserts - integer() comparison filters - json() equality filters - JSON update and reload behavior Helpful references Prisma query basics Prisma relation loading What gets generated Given this field: the Prisma output looks like: If you want the Prisma schema as a string instead of writing a file, use renderPrismaSchema(...) from @farming-labs/orm. The CLI page has a full example: CLI generation. For relations, the generator covers the direct cases cleanly: - belongsTo(...) renders the owning relation field with @relation(...) - hasOne(...) renders an optional inverse relation field - hasMany(...) renders an array relation field - join-table-backed manyToMany(...) still flows through the explicit join model Example generated model How it fits into a Prisma app Typical flow: 1. define schema in @farming-labs/orm 2. run farm-orm generate prisma 3. run normal Prisma commands in the app 4. create the typed runtime with createPrismaDriver({ client: prisma }) 5. keep library storage helpers on the unified API instead of rewriting them for Prisma In other words, Farming Labs ORM can now both feed Prisma and execute through Prisma. Replace mode vs block mode Replace mode Use this when the generated schema should own the whole output file. Block mode Use this when the application wants generated Prisma content inserted inside a managed block of an existing file. Local verification The repo verifies Prisma locally against SQLite, PostgreSQL, and MySQL with a real generated PrismaClient. Run it with: Demo in this repo There is also a small app-level demo that uses one shared auth flow and swaps the runtime underneath it. One of those adapters is the real generated Prisma client path, and it double-checks the created user with a direct Prisma query. Run it with: The same demo registry also supports: - memory - sqlite - postgres-pool - postgres-client - mysql-pool - mysql-connection - mongoose Runtime family The Prisma runtime now lives alongside the other shipped runtime packages: - Drizzle - Kysely - direct SQL - MongoDB - Mongoose The important constraint is the same across all of them: they plug into one shared query contract instead of redefining the API per stack. --- ## Redis & Upstash Redis URL: /docs/integrations/redis How to use Farming Labs ORM with Redis and Upstash Redis through one key-value runtime family. Redis and Upstash Redis Redis support in Farming Labs ORM is the key-value runtime family for teams that want one storage layer across: - a local or hosted Redis client - Upstash Redis - framework-owned cache, token, session, and rate-limit state The goal is not to turn Redis into a relational database. The goal is to keep one schema and one ORM surface when the storage contract is key-value friendly. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into Redis-backed operations with: - one schema definition - one query API - one runtime helper path - one capability surface - one normalized error surface This is a strong fit for sessions, verification tokens, cache metadata, rate limit buckets, lightweight billing or auth state, and other package-owned state that does not need heavy relational planning. Create the runtime directly Use the runtime helper path If a framework or package wants to accept the raw Redis client and normalize it later, use the runtime helper: That keeps the package boundary generic instead of forcing a Redis-specific adapter branch. Upstash Redis fits the same runtime family: Setup helpers Redis is a runtime-first backend, not a schema-push backend. That means: - createRedisDriver(...) works directly against the client - createOrmFromRuntime(...) works directly against the client - pushSchema(...) and applySchema(...) are intentional no-ops - bootstrapDatabase(...) is still useful when you want one entrypoint that prepares and returns the ORM Relations, lookups, and transactions Redis is not a join-first runtime, so the ORM stays explicit and conservative. That means: - nested relation selections work through follow-up reads - compound unique lookups and upserts still work - orm.transaction(...) is available - orm.$driver.capabilities.supportsTransactions is false So the runtime is a good fit for lightweight relational patterns inside a key-value workload, but it should not be presented as a native join runtime. What is supported well - string ids - manual numeric ids - integer() - json() - datetime() - enumeration() - decimal() - relation selections through follow-up reads - compound unique lookups and upserts - raw runtime detection - createOrmFromRuntime(...) - bootstrapDatabase(...) Important limits - generated integer ids are not supported - schema-qualified table names are not supported - there is no native join planner here - orm.transaction(...) should not be treated as a promise of full interactive rollback semantics in this runtime - Redis and Upstash are not the recommended fit for highly relational or join-heavy database workloads Why it matters This keeps Redis in the same bigger ORM story: - write your storage layer once - keep one schema definition - let the app choose Redis or Upstash - avoid owning a separate cache/token/session adapter surface --- ## Sequelize URL: /docs/integrations/sequelize How the unified schema and runtime fit into Sequelize-based PostgreSQL and MySQL apps. Sequelize Sequelize integration is runtime-first. Use @farming-labs/orm-sequelize when: - the app already owns a real Sequelize instance - a shared package wants to keep one storage layer across Sequelize, Prisma, Drizzle, Kysely, TypeORM, direct SQL, Firestore, MongoDB, or Mongoose - you want one schema definition and one query surface while still letting the app use Sequelize underneath Supported Sequelize dialect families - postgres / postgresql - mysql / mariadb The current repo verifies the live matrix on PostgreSQL and MySQL. MariaDB flows through the same MySQL-family path. Runtime setup From there, shared code keeps using the same unified API: What the Sequelize driver is doing The Sequelize driver does not invent a second schema system. It: 1. accepts the app's real Sequelize instance 2. executes through sequelize.query(...) and Sequelize transactions 3. reuses the shared SQL runtime semantics for filtering, mutations, relation loading, compound unique lookups, numeric IDs, namespaces, and normalized errors That means a package can write its storage layer once while each app decides whether the actual execution stack is Sequelize, TypeORM, Prisma, Drizzle, Kysely, direct SQL, Firestore, MongoDB, or Mongoose. Runtime helper path If a framework or shared package wants to accept the raw Sequelize client directly, use the runtime helpers: That is the cleanest path for higher-level integrations that do not want to branch on Sequelize specifically. Setup helpers The setup helpers work with Sequelize too: For Sequelize runtimes, that setup path renders safe SQL from the Farming Labs schema and applies it through the live connection. That is especially useful when a package or framework wants: - repeatable test setup - one bootstrap path across runtime families - no separate Sequelize-only schema-push API at the package boundary Relation support The Sequelize runtime inherits the current SQL-family relation behavior: - native single-query loading for supported singular chains - native single-query loading for simple hasMany(...) and explicit join-table manyToMany(...) branches without relation-level modifiers - shared fallback relation resolution for more complex relation branches that add their own where, orderBy, take, or skip That means auth-style and framework-style relation reads still work through the same unified API surface. Transactions and mutations Sequelize transactions map into the unified ORM transaction surface: The same runtime also supports: - create - createMany - update - updateMany - upsert - delete - deleteMany - compound-unique lookups - model-level constraint enforcement Local verification The repo verifies Sequelize locally against PostgreSQL and MySQL. Run it with: If you want to point the suite at your own local database URLs, use: You can also target a single Sequelize family while debugging: The PostgreSQL and MySQL paths create isolated temporary databases during the run and clean those databases up afterward. Why it fits well Sequelize already gives apps a familiar runtime abstraction. Farming Labs ORM sits one layer above that: - app code keeps Sequelize - package code keeps one schema and one storage layer - runtime helpers can still accept the raw Sequelize instance - setup helpers can still bootstrap the live database That is the main value: Sequelize apps can participate in the same package-level storage contract as TypeORM, Prisma, Drizzle, Kysely, direct SQL, Firestore, MongoDB, and Mongoose apps. --- ## SQL Databases URL: /docs/integrations/sql-databases How safe SQL generation and the direct SQL runtime fit PostgreSQL, MySQL, and SQLite workflows. SQL Databases Farming Labs ORM supports SQL-backed teams in 2 different ways: - safe SQL generation through @farming-labs/orm-cli - live direct SQL runtime through @farming-labs/orm-sql That means teams can either generate SQL artifacts, run the unified runtime against a real SQL client, or do both. This is the integration path where method translation is already live today. The same typed calls such as findUnique, findMany, create, update, upsert, and deleteMany are compiled by the SQL driver into the target dialect at runtime. Supported SQL dialects - postgres - mysql - sqlite Basic config Runtime package PostgreSQL with pg.Pool MySQL SQLite All 3 drivers expose the same unified query and mutation API: The direct SQL runtime also verifies: - integer() comparison filters - json() equality filters - compound-unique lookups and upserts - JSON update and reload behavior - collision-safe native projection aliases, so selected scalar fields stay intact even if a model also has a field name used by the native loader internally For relation loading, the SQL runtime now has a native single-query path for joinable singular branches such as: - session.user - session.user.profile - profile.user It also covers simple collection branches when the relation branch does not add its own where, orderBy, take, or skip, including: - direct hasMany(...) reads such as user.sessions - explicit join-table manyToMany(...) reads such as user.organizations That same native path is what the Drizzle and Kysely runtimes inherit, because both packages route through @farming-labs/orm-sql under the hood. Plural branches still work too, but they currently fall back to the shared relation resolver when they need relation-level filters, paging, or ordering. That fallback list should be read as the current implementation scope, not as a limitation of SQL itself. PostgreSQL, MySQL, and SQLite can all support richer native relation plans than the first singular path added here. The native loader also keeps its internal presence markers on collision-safe aliases. That means a real model field such as present can still be selected normally without conflicting with native row-mapping metadata. Example output For a simple user model, the SQL generator emits output like: If you want that SQL output as a string instead of writing a file, use renderSafeSql(...) from @farming-labs/orm. The CLI page includes the string render example: CLI generation. How this fits into PostgreSQL, MySQL, and SQLite workflows PostgreSQL Use: This is a good fit when the team wants schema-first SQL generation but does not need Prisma or Drizzle to be the consumer of that schema, or wants to pair the generated SQL with createPgPoolDriver(pool) at runtime. MySQL Use: This works well when the database is MySQL but the team prefers a more direct SQL workflow. It also pairs cleanly with createMysqlDriver(mysqlPool) if the team wants the unified runtime API instead of handwritten query code. SQLite Use: This is especially practical for lightweight apps, prototypes, demos, and local development workflows, and it can run directly through createSqliteDriver(sqliteDatabase). When the SQL path is a good fit - greenfield apps - database-first teams - simple deployment pipelines - projects that want generated DDL without standardizing on one ORM - libraries that want one runtime API and direct SQL backends Important limit The SQL generator currently emits safe SQL output. It is not trying to replace a full migration system with every advanced schema transform built in. That is why the docs call it safe SQL generation rather than a complete migration framework. Local verification The repo verifies the SQL runtime locally against SQLite, PostgreSQL, and MySQL. Run it with: Helpful references PostgreSQL driver docs MySQL driver docs SQLite runtime docs --- ## Supabase URL: /docs/integrations/supabase How to use Farming Labs ORM with Supabase through either its PostgreSQL connection layer or the direct Supabase JS client. Supabase Supabase now fits through two honest paths: - the PostgreSQL connection layer - the direct Supabase JS client runtime Both paths keep the same schema definition and the same ORM query surface. Which path to use Use the PostgreSQL path when: - the app already owns a pg pool or client - you want SQL-style setup helpers such as pushSchema(...) - you want the ORM to behave like the normal PostgreSQL runtime Use the direct Supabase JS path when: - the app already owns a createClient(...) instance - you want to accept that client directly instead of asking for a second DB client - you want the ORM to speak the Supabase table API rather than a hidden raw pg bridge PostgreSQL path If you already have a pg pool or client connected to Supabase, use the PostgreSQL aliases in @farming-labs/orm-sql. If you already have a connected PostgreSQL client instead of a pool: This path stays intentionally thin because it is still just PostgreSQL. Direct Supabase JS path If the app already has a Supabase client, use the dedicated runtime: Or let the runtime helpers detect it directly: This runtime uses Supabase's own client API. It does not wrap a hidden raw PostgreSQL bridge. Setup helpers If the package or framework needs to prepare the database before tests or setup flows, use the PostgreSQL path with the runtime-aware setup helpers: Or, if you want setup plus the ORM back: For the direct Supabase JS runtime, pushSchema(...) and applySchema(...) stay no-op. That path assumes the Supabase tables already exist. Generated output If the consuming app wants generated artifacts instead of only the live runtime, the same schema can still render: Connection guidance Supabase's own docs are still the right place to choose the actual connection mode, TLS settings, and connection string for your environment: - Connect to your database - Connection strings Keep the provider-recommended TLS settings for your environment. Avoid disabling certificate verification in connection examples unless you are debugging a local-only setup and fully understand the tradeoff. Why this matters This lets a library or framework: - keep one schema definition - accept either a raw PostgreSQL client or a Supabase client - reuse the same ORM query surface across both app styles - avoid forcing every Supabase app into the same connection strategy --- ## Support Matrix URL: /docs/integrations/support-matrix A simpler support overview for generation, live runtime support, and setup coverage across the supported stacks, including SurrealDB. Support Matrix This page is the quick answer to: - which stacks are supported - whether they are generation-first or runtime-first - whether the setup path is full, limited, or intentionally a no-op If you want the full story for one stack, use Integrations. This page is just the short support view. How to read this - Generation means Farming Labs ORM can render app-facing artifacts for that stack - Runtime means the unified ORM can execute against a real client for that stack - Setup means the runtime setup helpers are available for that stack Setup can mean different things depending on the backend: - relational stacks usually apply real schema or table changes - document or key-value stacks may use a lighter bootstrap path - some stacks intentionally keep setup as a no-op when there is no real DDL surface Basic matrix | Stack | Generation | Runtime | Setup | Notes | | --------------- | ---------- | ------- | ------- | --------------------------------------------------------------- | | Prisma | Yes | Yes | Full | Generated schema.prisma plus live PrismaClient runtime | | Drizzle | Yes | Yes | Full | Generated schema files plus live Drizzle runtime | | Kysely | No | Yes | Full | Runtime-first relational integration | | MikroORM | No | Yes | Full | Runtime-first relational integration | | TypeORM | No | Yes | Full | Runtime-first relational integration | | Sequelize | No | Yes | Full | Runtime-first relational integration | | EdgeDB / Gel | No | Yes | No-op | Runtime bridge through the official Gel SQL client | | Neo4j | No | Yes | Full | Graph runtime through the official Neo4j driver/session shapes | | SurrealDB | No | Yes | No-op | Multi-model runtime through the official SurrealDB client | | Cloudflare D1 | No | Yes | Limited | Worker-native SQLite binding with local/CI setup helpers | | Cloudflare KV | No | Yes | No-op | Worker-native key-value runtime | | Redis / Upstash | No | Yes | No-op | Key-value runtime for Redis and Upstash-compatible clients | | Direct SQL | Yes | Yes | Full | Safe SQL generation plus direct SQL runtime | | Supabase | Via SQL | Yes | Mixed | Works through raw PostgreSQL clients or the Supabase JS runtime | | Xata | No | Yes | Full | Direct Xata client runtime through its SQL surface | | MongoDB | No | Yes | Full | Native Mongo runtime | | Mongoose | No | Yes | Full | Mongoose-backed runtime | | Firestore | No | Yes | Limited | Real runtime, but no SQL-style schema push | | DynamoDB | No | Yes | Full | Creates one table per model through the runtime setup path | | Unstorage | No | Yes | No-op | Lightweight key-value/document runtime | Important details Relational stacks Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, and direct SQL are the strongest fit when the data model is relational or join-heavy. They also carry the best support for: - generated numeric IDs - richer native relation loading - Postgres-style schema namespaces - SQL-style setup and schema application Cloudflare D1 Cloudflare D1 is supported as its own runtime family rather than being folded into a generic SQL alias. That means: - pass the real Worker binding or a local Miniflare D1 binding directly - keep one schema definition and one query surface - use the same runtime helper path as the other stacks Two boundaries matter: - the runtime itself is Worker-friendly - the setup helpers fit best in local, CI, or other Node-managed bootstrap flows orm.transaction(...) is available, but the runtime does not claim full interactive rollback semantics the way a long-lived Node SQL connection can. EdgeDB / Gel EdgeDB support is a runtime bridge through the official Gel SQL client. That means: - pass the raw Gel client directly - keep one schema definition and one query surface - let the app keep its own Gel schema and migration flow Setup is intentionally conservative here: - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still gives the single convenience entrypoint - the runtime is for query execution, not Gel SDL management Neo4j Neo4j is supported as a runtime-first graph backend through the official Neo4j driver and session shapes. That means: - pass the raw Neo4j driver or session directly - keep one schema definition and one query surface - let the runtime setup helpers create the ORM-owned labels, constraints, and indexes Two boundaries matter: - this is not a Cypher-native graph query builder - relation loading stays conservative and uses follow-up reads instead of claiming native graph traversal planning SurrealDB SurrealDB is supported as a runtime-first multi-model backend through the official SurrealDB client. That means: - pass the real SurrealDB client directly - keep one schema definition and one query surface - use the same runtime helper path as the other backends Two boundaries matter: - the current ORM layer uses ORM-managed record and unique-lookup storage on top of the official client - relation loading stays conservative and uses follow-up reads instead of claiming native graph or SQL join planning Cloudflare KV Cloudflare KV is supported as its own runtime family rather than being folded into Redis or a generic document-store alias. That means: - pass the real Worker KVNamespace or a local Miniflare namespace directly - keep one schema definition and one query surface - rely on fallback relation reads and ORM-managed unique lookups Setup is intentionally light here: - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still gives the one-shot runtime entrypoint Redis / Upstash Redis Redis and Upstash Redis fit as a shared key-value runtime family. That means: - pass a Redis client or Upstash Redis client directly - keep one schema definition and one query surface - rely on fallback relation reads instead of a native join planner Setup is intentionally light here: - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still gives a convenient one-shot entrypoint This runtime is a strong fit for sessions, verification tokens, cache metadata, rate limits, and other lightweight package-owned state. It is not the recommended fit for highly relational or join-heavy workloads. Supabase Supabase now works through two honest paths: - a raw PostgreSQL client connected to Supabase - the direct Supabase JS runtime The PostgreSQL path keeps full SQL-style setup helpers. The direct Supabase JS runtime is query-first and keeps setup as a no-op. Xata Xata is supported as its own runtime family through the official Xata client. That means: - pass the real Xata client directly - keep one schema definition and one query surface - use the same runtime helper and setup path as the other SQL-backed stacks Two boundaries matter: - the runtime executes through Xata's SQL surface, not a hidden raw pg client - orm.transaction(...) stays conservative instead of claiming fully interactive rollback semantics Document and key-value runtimes MongoDB, Mongoose, Firestore, DynamoDB, Cloudflare KV, Redis / Upstash Redis, and Unstorage still work through the same unified ORM API, but they are not SQL runtimes. That means they rely more on: - follow-up relation reads instead of joins - ORM-managed lookup behavior - backend-specific setup limits Setup differences - Cloudflare KV keeps pushSchema(...) and applySchema(...) as no-ops - Firestore has a real runtime, but not a SQL-style schema push surface - DynamoDB setup creates one table per model - Unstorage intentionally keeps pushSchema(...) and applySchema(...) as no-ops because the storage contract is key-value oriented rather than DDL-oriented Numeric IDs and namespaces - generated numeric IDs are first-class on the relational runtimes, including D1 and Xata - Neo4j, SurrealDB, MongoDB, Mongoose, Firestore, DynamoDB, Cloudflare KV, Redis, and Unstorage keep numeric IDs manual-only - schema namespaces are only meaningful on Postgres-style relational runtimes Unstorage boundary Unstorage is useful for: - sessions - tokens - lightweight records - package-owned state It is not the recommended runtime for highly relational or join-heavy database workloads. Where to go next Integrations · Framework Authors --- ## SurrealDB URL: /docs/integrations/surrealdb How to use Farming Labs ORM with the official SurrealDB client through one schema and one shared runtime surface. SurrealDB SurrealDB integration is runtime-first. Use @farming-labs/orm-surrealdb when: - the app already owns a real SurrealDB client - a shared package wants to keep one storage layer across SurrealDB, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Neo4j, Firestore, DynamoDB, Redis, Unstorage, or MongoDB-style runtimes - you want one schema definition and one query surface while still letting the app keep SurrealDB underneath Supported SurrealDB runtime inputs - an official surrealdb Surreal client - the same client passed directly into createOrmFromRuntime(...) The current runtime uses the official client directly. It does not invent another hidden SQL or graph client layer. Runtime setup From there, shared code keeps using the same unified API: What the SurrealDB driver is doing The SurrealDB driver keeps the shared ORM surface and stores records through the official client using ORM-managed record and unique-lookup entries. It: 1. accepts the app's real SurrealDB client 2. executes reads and writes through the official client and transaction API 3. keeps the same normalized error and capability surface as the rest of the repo 4. resolves relations conservatively through follow-up lookups That means a package can write its storage layer once while each app decides whether the actual execution stack is SurrealDB, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Neo4j, Firestore, DynamoDB, Redis, Unstorage, MongoDB, or Mongoose. Runtime helper path If a framework or shared package wants to accept the raw SurrealDB client directly, use the runtime helpers: That is the cleanest path for higher-level integrations that do not want to branch on SurrealDB specifically. Setup helpers The setup helpers still work with SurrealDB: For SurrealDB runtimes, pushSchema(...) and applySchema(...) are intentional no-ops. That is useful when a package or framework wants: - one setup path across runtime families - no separate SurrealDB-only setup API at the package boundary - a single bootstrapDatabase(...) entrypoint even when the backend has no SQL-style DDL workflow Transactions and limits The current SurrealDB runtime supports normal read and write transactions through the official client's transaction API. That means: - create - createMany - update - updateMany - upsert - delete - deleteMany - compound-unique lookups - model-level constraint enforcement - orm.transaction(...) Two boundaries still matter: - generated integer IDs are not supported - schema-qualified table namespaces are not supported Relation support SurrealDB is a multi-model database, but the current ORM layer stays conservative about relation loading. That means: - belongsTo - hasOne - hasMany - explicit join-table manyToMany work through the shared relation resolver and follow-up lookups instead of claiming native graph or SQL join planning. That makes the current runtime a strong fit for: - auth and account state - org/member/workspace state - app and framework records - package-owned storage layers that still want one ORM API Local verification Run the local SurrealDB suite with: That package-local suite uses a real embedded mem:// SurrealDB client through the official SDK, so the runtime, helper, relation, transaction, and duplicate-key paths are exercised against a live local backend instead of a fake harness. --- ## TypeORM URL: /docs/integrations/typeorm How the unified schema and runtime fit into TypeORM DataSource-based apps. TypeORM TypeORM integration is runtime-first. Use @farming-labs/orm-typeorm when: - the app already owns a real TypeORM DataSource - a shared package wants to keep one storage layer across TypeORM, Prisma, Drizzle, Kysely, SQL, Firestore, MongoDB, or Mongoose - you want one schema definition and one query surface while still letting the app use TypeORM underneath Supported TypeORM dialect families - postgres - mysql / mariadb - sqlite-family DataSources such as sqlite, better-sqlite3, and sqljs The current repo verifies the broadest TypeORM matrix on PostgreSQL and MySQL, with SQL.js-backed SQLite smoke coverage for bootstrap and runtime creation. Runtime setup From there, shared code keeps using the same unified API: What the TypeORM driver is doing The TypeORM driver does not invent another ORM layer. It: 1. accepts the app's real DataSource 2. executes through TypeORM query runners and transactions 3. reuses the shared SQL runtime semantics for filtering, mutations, relation loading, compound unique lookups, numeric IDs, and normalized errors That means a package can write its storage layer once while each app decides whether the actual execution stack is TypeORM, Prisma, Drizzle, Kysely, direct SQL, Firestore, MongoDB, or Mongoose. Runtime helper path If a framework or shared package wants to accept the raw DataSource directly, use the runtime helpers: That is the cleanest path for higher-level integrations that do not want to branch on TypeORM specifically. Setup helpers The setup helpers work with TypeORM too: For TypeORM DataSources, that setup path renders safe SQL from the Farming Labs schema and applies it through the DataSource itself. That is especially useful when a package or framework wants: - repeatable test setup - one bootstrap path across runtime families - no separate TypeORM-only schema-push API at the package boundary Relation support The TypeORM runtime inherits the current SQL-family relation behavior: - native single-query loading for supported singular chains - native single-query loading for simple hasMany(...) and explicit join-table manyToMany(...) branches without relation-level modifiers - shared fallback relation resolution for more complex relation branches that add their own where, orderBy, take, or skip That means auth-style and framework-style relation reads still work through the same unified API surface. Transactions and mutations TypeORM transactions map into the unified ORM transaction surface: The same runtime also supports: - create - createMany - update - updateMany - upsert - delete - deleteMany - compound-unique lookups - model-level constraint enforcement Local verification The repo verifies TypeORM locally against PostgreSQL and MySQL, with SQLite smoke coverage for runtime and bootstrap creation. Run it with: If you want to point the suite at your own local database URLs, use: You can also target a single TypeORM family while debugging: The PostgreSQL and MySQL paths create isolated temporary databases during the run, execute the real TypeORM-backed runtime against them, and then clean those databases up afterward. Why it fits well TypeORM already gives apps a familiar DataSource abstraction. Farming Labs ORM sits one layer above that: - app code keeps TypeORM - package code keeps one schema and one storage layer - runtime helpers can still accept the raw DataSource - setup helpers can still bootstrap the live database That is the main value: TypeORM apps can participate in the same package-level storage contract as Prisma, Drizzle, Kysely, direct SQL, Firestore, MongoDB, and Mongoose apps. --- ## Unstorage URL: /docs/integrations/unstorage How to use Farming Labs ORM with Unstorage through one unified key-value/document runtime. Unstorage Unstorage support in Farming Labs ORM is the key-value/runtime answer for teams that want one storage layer across lightweight stores without owning a separate adapter surface for each Unstorage driver. The supported path is a raw Unstorage storage instance created with createStorage(...). That makes it a good fit for: - sessions - verification tokens - lightweight user/account records - framework-owned state - prototypes and smaller storage footprints It is not the runtime to reach for when the workload is highly relational, join-heavy, or dependent on strong database transaction guarantees. What this gives you You still write the schema and storage layer once. Then the runtime translates that layer into Unstorage operations with: - one schema definition - one query API - one runtime-helper path - one capability surface - one normalized error surface Create the runtime directly You can use any Unstorage storage instance that exposes the standard getItem(...), setItem(...), removeItem(...), and getKeys(...) shape. Use the runtime helper path If a framework or shared package wants to accept a raw Unstorage instance and normalize it later, use the runtime helpers instead: That keeps the package boundary generic instead of forcing a storage-specific branch. Setup helpers The setup helpers are intentionally conservative here: - pushSchema(...) is a no-op - applySchema(...) is a no-op - bootstrapDatabase(...) still works and returns the ORM client after that no-op setup stage That reflects the real backend shape: Unstorage is a generic key-value/document abstraction, not a SQL-style schema engine. Joins, relations, and transactions Unstorage does not have native joins. That does not mean relation selections stop working. The ORM resolves relation branches through follow-up reads, so the same higher-level storage code can still select related records. It is still important to be honest about the boundary: - relation loading is fallback-based, not native-join based - orm.$driver.capabilities.supportsTransactions is false - orm.transaction(...) stays available as a storage-layer boundary, not as a promise of full database transaction semantics So this runtime works well for shared package logic that needs one consistent storage contract, but it is not the best fit for workloads that are deeply relational or centered around complex join-heavy query plans. What is supported well - string ids - manual numeric ids - integer() - json() - enumeration() - bigint() - decimal() - compound unique lookups and upserts - relation selections through follow-up queries - raw client detection - createOrmFromRuntime(...) - normalized duplicate-key errors Important limits - generated integer ids are not supported on Unstorage - schema-qualified table names are not supported - relation loading is fallback-based, not native-join based - setup helpers do not create real tables or indexes - non-unique filtering is key-scan based - this runtime is not meant for highly relational or join-heavy database workloads Local verification The repo verifies the Unstorage runtime locally with the in-memory and fs-lite drivers. Run it with: Why it matters This keeps Unstorage inside the same bigger ORM story: - write your storage layer once - keep one schema definition - let the app choose a lightweight Unstorage-backed store - avoid owning a separate adapter surface for every storage driver --- ## Xata URL: /docs/integrations/xata How to use Farming Labs ORM with the official Xata client through one schema and one shared runtime surface. Xata Xata integration is runtime-first. Use @farming-labs/orm-xata when: - the app already owns a real Xata client - a shared package wants to keep one storage layer across Xata, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Neo4j, Firestore, DynamoDB, Redis, or MongoDB-style runtimes - you want one schema definition and one query surface while still letting the app keep Xata underneath Supported Xata runtime inputs - an official Xata client that exposes client.sql(...) - the same client passed directly into createOrmFromRuntime(...) The current runtime uses Xata's own SQL surface directly. It does not invent another hidden PostgreSQL client. Runtime setup From there, shared code keeps using the same unified API: What the Xata driver is doing The Xata driver keeps the shared ORM surface and translates it through client.sql(...) against Xata's SQL layer. It: 1. accepts the app's real Xata client 2. executes reads and writes through Xata's SQL API 3. keeps the same normalized error and capability surface as the rest of the repo 4. preserves Postgres-style features such as generated numeric IDs and schema namespaces That means a package can write its storage layer once while each app decides whether the actual execution stack is Xata, Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, direct SQL, Supabase, Neo4j, Firestore, DynamoDB, Redis, MongoDB, or Mongoose. Runtime helper path If a framework or shared package wants to accept the raw Xata client directly, use the runtime helpers: That is the cleanest path for higher-level integrations that do not want to branch on Xata specifically. Setup helpers The setup helpers work with Xata too: For Xata runtimes, that setup path renders safe PostgreSQL DDL and applies it through the Xata client. That is useful when a package or framework wants: - repeatable local or CI bootstrap - one setup path across runtime families - no separate Xata-only setup API at the package boundary Transactions and limits The current Xata runtime is intentionally conservative about transactions. That means: - the runtime supports normal reads, writes, upserts, and compound-unique lookups - pushSchema(...) and bootstrapDatabase(...) work through the same runtime helper path - orm.transaction(...) does not claim full long-lived interactive rollback semantics the way a direct Node PostgreSQL connection can This keeps the runtime honest while still making it useful for real package and framework storage layers. Numeric IDs and namespaces Because the Xata runtime uses the PostgreSQL SQL surface, it supports the same high-value pieces many shared packages care about: - generated integer IDs through id({ type: "integer", generated: "increment" }) - Postgres-style schema-qualified tables through tableName("users", { schema: "auth" }) - SQL-style compound unique behavior Local verification Run the local Xata suite with: That package-local suite uses a real local PostgreSQL-backed Xata-shaped runtime harness so the query, setup, generated numeric ID, and namespace paths are all exercised against a live SQL backend. If you also want to verify the runtime against a real Xata project, run: That opt-in suite talks to the official Xata client directly and is meant for a dedicated test branch or database URL. --- ## Runtime URL: /docs/runtime What runtime means in Farming Labs ORM and how it differs from generation. 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 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: The handle also exposes read-only capability metadata: That 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(...): If detection fails, use the diagnostic report instead of guessing: If you want the same fallback behavior in one step, use @farming-labs/orm-runtime: That 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, Xata, MongoDB, Mongoose, Firestore, DynamoDB, and Unstorage runtimes without inventing a separate config surface. There is also a dedicated guide for this flow: - Runtime Helpers - Framework Authors 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: - findUnique - findOne - findFirst - findMany - count - create - createMany - update - updateMany - upsert - delete - deleteMany - transaction - batch Relations All live runtime drivers support: - belongsTo - hasOne - hasMany - 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(...) and hasOne(...) - simple hasMany(...) and explicit join-table manyToMany(...) branches when the relation branch does not add its own where, orderBy, take, or skip - Prisma translates supported nested relation branches directly into Prisma delegate select trees, including simple explicit join-table manyToMany(...) 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 as email or token - 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, Xata, 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/orm - createPrismaDriver(...) from @farming-labs/orm-prisma - createDrizzleDriver(...) from @farming-labs/orm-drizzle - createKyselyDriver(...) from @farming-labs/orm-kysely - createMikroormDriver(...) from @farming-labs/orm-mikroorm - createTypeormDriver(...) from @farming-labs/orm-typeorm - createSequelizeDriver(...) from @farming-labs/orm-sequelize - createEdgeDbDriver(...) from @farming-labs/orm-edgedb - createD1Driver(...) from @farming-labs/orm-d1 - createKvDriver(...) from @farming-labs/orm-kv - createRedisDriver(...) from @farming-labs/orm-redis - createSupabaseDriver(...) from @farming-labs/orm-supabase - createSqliteDriver(...) from @farming-labs/orm-sql - createMysqlDriver(...) from @farming-labs/orm-sql - createPgPoolDriver(...) from @farming-labs/orm-sql - createPgClientDriver(...) from @farming-labs/orm-sql - createSupabasePoolDriver(...) from @farming-labs/orm-sql - createSupabaseClientDriver(...) from @farming-labs/orm-sql - createFirestoreDriver(...) from @farming-labs/orm-firestore - createMongoDriver(...) from @farming-labs/orm-mongo - createMongooseDriver(...) from @farming-labs/orm-mongoose - createDynamodbDriver(...) from @farming-labs/orm-dynamodb - createUnstorageDriver(...) from @farming-labs/orm-unstorage - createOrmFromRuntime(...) from @farming-labs/orm-runtime - createDriverFromRuntime(...) from @farming-labs/orm-runtime - pushSchema(...) from @farming-labs/orm-runtime/setup - applySchema(...) from @farming-labs/orm-runtime/setup - bootstrapDatabase(...) 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. 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: Local verification pnpm test already includes these real integration suites. Use the commands below when you want to rerun the database-backed paths directly. Continue - Runtime Helpers - Query API - Memory driver - Schema relations --- ## Memory Driver URL: /docs/runtime/memory-driver The current live runtime driver for tests, demos, and docs examples. Memory Driver createMemoryDriver(...) is the runtime driver implemented in this repo today. It is ideal for: - tests - seeded demos - docs examples - validating the typed query API Basic usage Seeded usage What it supports - model CRUD - generated/default field application - nested relation selection - filtering - sorting - paging - transaction rollback on thrown errors How defaults work When you call create(...), the memory driver will: - generate ids for id() - generate dates for defaultNow() - apply .default(...) values - keep provided values when you pass them explicitly Example Even though id and timestamps are not passed above, the driver can still fill them when the schema marks them as generated/defaulted. Relation support The memory driver already understands: - belongsTo - hasOne - hasMany - manyToMany That is why it is so useful as a reference driver for the runtime API. Transaction behavior The memory driver snapshots its current state before transaction(...) runs. If the callback throws, it restores the snapshot. After the error, the transaction changes are rolled back. What it is not meant for - production persistence - large-scale performance work - replacing a real database Its role is to make the query API concrete and testable while live drivers are still being layered in. --- ## Query API URL: /docs/runtime/query-api Typed query and mutation methods, nested select, filters, sorting, paging, transactions, and auth-style workflows. Query API The runtime API is model-based and fully typed from the schema. Model clients Given: you get model clients such as: Each model client supports: - findUnique - findOne - findMany - findFirst - count - create - createMany - update - updateMany - upsert - delete - deleteMany findUnique Use findUnique when the query should resolve to at most one record, such as email, id, token, or another unique lookup. That includes declared compound unique keys. For findUnique(...), the where object should match exactly one unique key: - an id field - a single .unique() field - or a declared model-level compound unique constraint findOne findOne is the simple ergonomic name for "give me the first matching record". It is useful for library code that does not want to expose Prisma-flavored method names to its own consumers. findFirst Returns one typed record or null. findFirst is still available for teams that prefer that naming. In the current runtime it behaves the same as findOne. findMany Returns an array of typed records. count Returns the number of matching rows without loading the full records. create Generated/default fields are filled by the driver where supported. In the memory driver: - id() generates an id - defaultNow() uses new Date() - .default(...) applies the literal default value createMany Returns the created records, narrowed by select if provided. update Returns the updated record or null. updateMany Returns the number of updated rows. upsert This is especially useful for auth-style operations such as rotating sessions, refreshing tokens, or syncing provider accounts. It also supports compound unique keys: Integer and JSON filters Integer fields participate in the normal comparison operators: JSON fields can be written and read like any other scalar, and equality filters can use the raw JSON value directly: This is especially useful for auth and plugin data where you want a stable typed contract in library code, but the underlying app may be using Prisma, Drizzle, Kysely, direct SQL, MongoDB, or Mongoose. For compound upserts: - every field in where must belong to one declared unique key - create must agree with the where values for those fields - update cannot change the conflict fields themselves Enum, bigint, and decimal values Enums behave like constrained strings in the unified API: Bigints use real JavaScript bigint values and support the normal comparison operators: Decimals stay as strings so precision is preserved across runtimes: Decimal outputs are normalized strings, so values like "12.50" may reload as "12.5" after a round-trip. delete Returns the number of deleted rows. deleteMany Returns the number of deleted rows across the whole match set. select and type inference The select object is one of the most important parts of the runtime API. That means the result type is narrowed to exactly the selected fields and relations. Relation selection Relations can be selected with: - true for default scalar selection on the target - a nested object with select, where, orderBy, take, and skip Example where filters Shorthand equality Operator objects Supported operators For strings: - eq - contains - in - not For comparable values such as strings and dates: - eq - gt - gte - lt - lte - in - not Logical operators Sorting and paging Transactions Batch workflows batch is helpful when a library wants one grouped storage call but still wants to keep each internal query simple and typed. Auth-style storage example If the storage layer normalizes emails, normalize them on write as well as on lookup. A lookup-only toLowerCase() can miss legacy or mixed-case rows. This is the main point of the runtime contract: an auth package can write its storage layer once and then plug that same logic into whichever runtime driver the app eventually uses. Design goals of the query API - model-first instead of stringly-typed command objects - typed select result narrowing - generic enough to support many driver implementations - simple enough to explain in library docs Related pages - Runtime overview - Memory driver - Schema relations --- ## Runtime Helpers URL: /docs/runtime/runtime-helpers Create a driver or ORM directly from a raw Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, EdgeDB / Gel, Neo4j, SurrealDB, Cloudflare D1, Cloudflare KV, Redis / Upstash, Supabase, Xata, SQL, Firestore, DynamoDB, Unstorage, MongoDB, or Mongoose runtime. Runtime Helpers @farming-labs/orm-runtime is the light, lazy convenience layer on top of createOrm(...). It accepts a raw runtime instance, detects what it is, creates the matching driver, and then builds the ORM for you. Why it exists The core createOrm(...) API stays explicit: The runtime helper is for higher-level integrations that want to accept the raw client directly: That is especially useful when another library wants to accept: - a PrismaClient - a Drizzle database - a Kysely instance - an EdgeDB / Gel SQL client - a Neo4j driver or session - a SurrealDB client - a MikroORM instance or EntityManager - a TypeORM DataSource - a Sequelize instance - a Cloudflare D1 binding - a Cloudflare KV namespace - a Redis or Upstash Redis client - a Supabase client created with createClient(...) - a Xata client with client.sql(...) - a pg pool/client - a mysql2 pool/connection - a Firestore client - a DynamoDBClient or DynamoDBDocumentClient - an Unstorage createStorage(...) instance - a Mongo Db or MongoClient - a Mongoose connection createOrmFromRuntime(...) This is the highest-level helper. It does four things: 1. detects the runtime with the same logic as detectDatabaseRuntime(...) 2. creates the right Farming ORM driver 3. calls createOrm(...) 4. returns the normal typed ORM client The returned client still exposes the same generic capability and normalized error surface as any other Farming ORM client through orm.$driver.capabilities and OrmError. Capabilities and normalized errors The runtime helper does not return a special client shape. Once the ORM is created, it behaves like any other Farming ORM client. Generated integer IDs are now first-class in the supporting runtimes too: If runtime detection fails, ask for the structured report: pushSchema(...), applySchema(...), and bootstrapDatabase(...) The runtime-aware setup flow lives on the Node-only setup subpath. That matters for Cloudflare D1 in particular: the live runtime path works in a Worker, while the setup helpers fit best in local, CI, or other Node-managed bootstrap flows. Use them like this: - pushSchema(...) Ensures the current schema is pushed into the live runtime. - applySchema(...) Applies the current schema through the same runtime-aware setup path. - bootstrapDatabase(...) Pushes the schema, then returns createOrmFromRuntime(...). When setup fails, the setup subpath throws RuntimeSetupError with the stage, detected runtime kind, dialect, and original cause preserved: That means one package now covers: 1. detect the runtime 2. create the driver 3. create the ORM 4. prepare the live database or collections SQL-family runtimes For direct SQL, Drizzle, and Kysely, the helper renders safe SQL DDL and applies it to the detected dialect. Cloudflare D1 For Cloudflare D1, pass the Worker binding or a local Miniflare D1 binding directly: The D1 setup path uses the same schema and the same helper surface, but it is best used in local or CI flows because @farming-labs/orm-runtime/setup stays Node-only. EdgeDB / Gel For EdgeDB, pass the official Gel SQL client directly: This runtime is intentionally query-first: - createOrmFromRuntime(...) detects the Gel SQL client directly - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still returns the ORM, but assumes the Gel schema already exists Neo4j For Neo4j, pass the official driver or session directly: This runtime is intentionally graph-aware but conservative: - createOrmFromRuntime(...) detects the Neo4j driver or session directly - pushSchema(...) and applySchema(...) create ORM-owned constraints and indexes - relation loading stays on the shared resolver path instead of claiming Cypher-native graph traversal planning Cloudflare KV For Cloudflare KV, pass the namespace directly: The Cloudflare KV runtime keeps setup intentionally light: - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still works as the single convenience entrypoint - relation loading uses follow-up reads instead of a native join planner Redis / Upstash Redis For Redis and Upstash Redis, pass the client directly: The Redis runtime keeps setup intentionally light: - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still works as the single convenience entrypoint - relation loading uses follow-up reads instead of a native join planner Supabase For the direct Supabase JS path, pass the client created with createClient(...) directly: This path is intentionally query-first: - pushSchema(...) and applySchema(...) are no-ops - bootstrapDatabase(...) still works as the single convenience entrypoint - the runtime uses the Supabase table API instead of a hidden raw pg bridge If the app already owns a raw PostgreSQL client connected to Supabase, use the normal SQL runtime path instead. Xata For Xata, pass the official client directly: This runtime stays SQL-backed but conservative: - createOrmFromRuntime(...) detects the Xata client through its SQL surface - pushSchema(...) and applySchema(...) render PostgreSQL-safe SQL and apply it through the Xata client - orm.transaction(...) does not overclaim interactive rollback semantics Prisma For Prisma, the helper renders a temporary Prisma schema and runs prisma db push --skip-generate under the hood. If your Prisma client does not expose a usable datasource URL, pass one explicitly: MongoDB and Mongoose For MongoDB and Mongoose, the helper creates the declared collections and ensures unique/index metadata from the schema manifest. DynamoDB For DynamoDB, the helper creates one table per model through the raw AWS client. The DynamoDB runtime then stores: - record items - internal unique-lock items inside that model table so exact unique and compound-unique lookups do not force higher-level packages to reimplement locking logic themselves. Unstorage For Unstorage, pushSchema(...) and applySchema(...) are intentional no-ops. That is the honest behavior for this runtime: - it works well for lightweight key-value/document storage - it keeps the same storage-layer API surface as the other runtimes - it is not meant for highly relational or join-heavy database workloads createDriverFromRuntime(...) If you want the driver but still want to call createOrm(...) yourself, use the lower-level helper: Supported runtime inputs @farming-labs/orm-runtime currently supports: - Prisma client - Drizzle database - Kysely instance - EdgeDB / Gel SQL client - MikroORM instance or EntityManager - TypeORM DataSource - Sequelize instance - Cloudflare D1 binding - Cloudflare KV namespace - Redis client - Upstash Redis client - Supabase client - SQLite database - pg Pool - pg Client - mysql2 Pool - mysql2 Connection - Firestore client - DynamoDBClient - DynamoDBDocumentClient - Unstorage storage - Mongo Db - Mongo MongoClient - Mongoose connection Examples Prisma Direct SQL Drizzle For PostgreSQL and MySQL, Drizzle usually carries enough runtime information on its own. For Drizzle SQLite, pass the raw client explicitly too: Kysely TypeORM If you already have a TypeORM DataSource, pass it directly: Sequelize If you already have a Sequelize instance, pass it directly: MongoDB If you pass a Db, the helper can use it directly: If you pass a MongoClient, also provide databaseName: Mongoose If you pass a Mongoose connection, the helper will infer models by collection name from the schema manifest. If you want to control the exact models or transforms, pass overrides: Unstorage If you already have an Unstorage instance, pass it directly: Use this when you want one storage layer over a lightweight key-value/document backend. Prefer the SQL-family runtimes when the workload is deeply relational or join-heavy. Relationship with detectDatabaseRuntime(...) These helpers build directly on top of runtime detection. Use detectDatabaseRuntime(...) when you only want inspection. Use createDriverFromRuntime(...) or createOrmFromRuntime(...) when you want to execute queries. Use pushSchema(...), applySchema(...), or bootstrapDatabase(...) from @farming-labs/orm-runtime/setup when you also want the runtime helper package to prepare the live database. Real local verification The repo verifies this helper against live local runtimes, not a fake adapter layer. That matrix includes: - direct SQL on SQLite, PostgreSQL, and MySQL - Drizzle on SQLite, PostgreSQL, and MySQL - Kysely on SQLite, PostgreSQL, and MySQL - TypeORM on SQLite-family, PostgreSQL, and MySQL - Sequelize on PostgreSQL and MySQL - Prisma on SQLite, PostgreSQL, and MySQL - Firestore - DynamoDB through a local Dynalite-backed suite - Unstorage through the in-memory and fs-lite drivers - MongoDB - Mongoose Continue - Runtime - Query API - Getting Started --- ## Unified Schema URL: /docs/schema One source of truth for fields, constraints, defaults, references, and relationships. Unified Schema The schema DSL is the center of @farming-labs/orm. Everything else either reads the schema directly or consumes its normalized manifest form. Core building blocks - defineSchema(...) defines the top-level schema object - model(...) defines one model - tableName(...) builds a structured table reference when you need a namespace - field builders such as id(), string(), boolean(), and datetime() - relation helpers such as belongsTo(...), hasOne(...), hasMany(...), and manyToMany(...) - model-level constraints such as compound unique keys and indexes Full schema example Namespaced tables Flat table names still work: When you need a Postgres schema namespace, use tableName(...): That keeps the logical model name as user while generators and the SQL runtime emit qualified identifiers like "auth"."users". Use the structured helper instead of passing "auth.users" as a flat string. Schema-qualified strings are rejected so the ORM can keep table names and namespaces unambiguous. Generated numeric IDs are currently first-class on the SQL-family runtimes, Prisma, and the in-memory driver. MongoDB and Mongoose keep manual numeric IDs only for now. Field builders Scalar field types | Builder | Output type | Notes | | ------------------------------------------------- | ----------- | ------------------------------------------------ | | id() | string | Unique by default and generated as an id | | id({ type: "integer" }) | number | Manual numeric primary key support | | id({ type: "integer", generated: "increment" }) | number | Auto-generated numeric primary key support | | string() | string | Plain string scalar | | boolean() | boolean | Boolean scalar | | datetime() | Date | Represents a JavaScript Date in runtime typing | Field methods | Method | Purpose | | ---------------------------- | ------------------------------------------------------- | | .unique() | Marks the field unique | | .nullable() | Makes the runtime output T \| null | | .default(value) | Stores a literal default | | .defaultNow() | Marks the field as generated from now | | .references("model.field") | Declares a foreign-key style reference | | .map("column_name") | Maps a field name to a different physical column name | | .describe("...") | Adds documentation metadata for the manifest/docs layer | Model-level constraints Use model constraints when uniqueness or indexing belongs to a combination of fields instead of a single field. Constraint keys | Key | Purpose | | --------- | --------------------------------------- | | unique | Declares one or more unique field sets | | indexes | Declares one or more non-unique indexes | Why this matters - generators emit real compound uniques and indexes - the runtime now understands declared compound unique keys - findUnique(...) and upsert(...) can use those same field sets directly Example: Relation helpers belongsTo Use this when the current model holds the foreign key. hasOne Use this when the target model stores a unique foreign key back to the current model. hasMany Use this when the target model stores a non-unique foreign key back to the current model. manyToMany Use this when you have a join model and want higher-level relation metadata. In plain English: - through is the join model - from is the join-table field pointing back to the current model - to is the join-table field pointing at the target model Mapped names and why they matter This field: means: - the logical field name is email - the physical column name is email_address - the typed client still uses email - generators output the mapped database name What generators use vs. what runtime uses Today, it is important to separate two layers of value: - Generators primarily use field-level manifest data such as column names, defaults, uniqueness, and references - Runtime helpers can use relation metadata such as hasMany(...) and manyToMany(...) That means the current schema relation graph is already useful, but not every generator consumes every relation helper yet. Manifest layer Internally, the schema is normalized into a manifest that contains: - model names and table names - field names and mapped columns - scalar kinds - nullability - uniqueness - compound unique constraints - indexes - generated/default values - references - descriptions That is the handoff point used by the code generators. Design guidance - Keep model names logical and stable - Use .map(...) only when the physical database naming must differ - Prefer references(...) whenever a foreign-key relationship is real - Use relation helpers to express how the library thinks about the graph, not just how the database stores it - Keep backend-specific translation logic out of the library package whenever possible --- ## Fields URL: /docs/schema/fields Field builders, defaults, mapping, nullability, uniqueness, and references. Fields Field builders define the scalar shape of each model. Scalar builders id() - runtime output type: string - unique by default - generated as an id by default For numeric primary keys, pass an explicit type: - runtime output type: number - useful when an app or integration needs integer-shaped identifiers - works well for manually assigned numeric IDs If you want the runtime or database to generate integer IDs for you, opt into increment semantics: - runtime output type: number - SQL, Drizzle, Kysely, Prisma, and the memory driver can generate the value - MongoDB and Mongoose still require manual numeric IDs today string() - runtime output type: string boolean() - runtime output type: boolean datetime() - runtime output type: Date integer() - runtime output type: number enumeration() - runtime output type: the string union from the values you pass in - generated as a real enum where the target supports it - TypeScript reserves the enum keyword, so the builder is named enumeration() bigint() - runtime output type: bigint - useful for 64-bit counters, quotas, and ids that should not round through number decimal() - runtime output type: string - keeps precision stable across runtimes instead of routing decimal math through JavaScript number - decoded values are normalized strings, so "12.50" may round-trip as "12.5" json() - runtime output type: the JSON shape you pass to json() - useful for provider metadata, settings blobs, and structured plugin state Field modifiers .unique() Marks the field as unique in the schema and generated outputs. Use this for single-field uniqueness. For compound uniqueness such as provider + accountId, use model-level constraints.unique on model(...) instead. .nullable() Changes the runtime output type from string to string | null. .default(...) Stores a literal default value. This works well for string, boolean, integer, bigint, decimal, and enum fields today. JSON defaults can still be applied by runtime drivers, but generated schema support is more limited and should be treated as backend-specific for now. .defaultNow() Marks the field as generated from the current time. .references("model.field") Adds foreign-key-style metadata that generators can translate. .map("column_name") Keeps the logical API field name as email while generating the physical column name email_address. .describe("...") Adds descriptive metadata to the manifest/docs layer. Example model Numeric IDs use the same DSL: Generated numeric IDs use the same builder: What generators care about Field data is especially important because it drives: - Prisma field types - Drizzle column definitions - SQL column definitions - nullability - unique constraints - references - generated/default behavior The current generators and live runtimes support: - integer() across Prisma, Drizzle, safe SQL, memory, Prisma runtime, SQL, Kysely, Drizzle, MongoDB, and Mongoose - json() across Prisma, Drizzle, safe SQL, memory, Prisma runtime, SQL, MongoDB, and Mongoose - enumeration() across Prisma, Drizzle, safe SQL, memory, Prisma runtime, SQL, Kysely, MongoDB, and Mongoose - bigint() across Prisma, Drizzle, safe SQL, memory, Prisma runtime, SQL, Kysely, MongoDB, and Mongoose - decimal() across Prisma, Drizzle, safe SQL, memory, Prisma runtime, SQL, Kysely, MongoDB, and Mongoose For SQLite-backed bigint tests, the local matrix enables the underlying node:sqlite big-int read mode so values do not get truncated back into JavaScript number. Mapping example Logical side: - user.email Physical side: - users.email_address This is a big part of why the schema DSL is useful for reusable packages: the library can keep a stable logical field name even when the physical database surface needs a different name. --- ## Relations URL: /docs/schema/relations One-to-one, one-to-many, and many-to-many relation patterns. Relations Relations describe how models connect at the library level. In the current repo: - field references are already used heavily by generators - relation helpers are already used heavily by the runtime layer Relation helpers - belongsTo(target, { foreignKey }) - hasOne(target, { foreignKey }) - hasMany(target, { foreignKey }) - manyToMany(target, { through, from, to }) One-to-one One-to-one usually means: - one side stores a unique foreign key - the other side exposes a hasOne(...) relation Example Plain English - profile.userId points to user.id - each profile can only point to one user - because profile.userId is unique, each user can only have one profile One-to-many One-to-many usually means: - many rows on the child side store the same foreign key - the parent exposes hasMany(...) - the child exposes belongsTo(...) Example Plain English - many sessions can point to one user - each session belongs to exactly one user Many-to-many Many-to-many uses a join model. Example What through, from, and to mean For this relation: the relation is read from the perspective of the current model, user: - through: "membership" means the join model is membership - from: "userId" means membership.userId points back to the current model - to: "organizationId" means membership.organizationId points to the target model That is why the reverse side would swap them. Runtime selection examples One-to-one One-to-many Current generator note Relation helpers are still most powerful in the live runtime layer, but generation is no longer field-only: - Prisma generation now covers direct belongsTo, hasOne, and hasMany - Drizzle generation now covers direct belongsTo, hasOne, and hasMany - explicit join-table manyToMany still stays explicit through the join model in generated Prisma and Drizzle output That is why the docs still separate runtime relation behavior from generated ORM artifacts: direct relations are covered, but join-table convenience traversal is still a runtime concern. --- ## Use Cases URL: /docs/use-cases Where the generator-first approach fits best. Use Cases The strongest fit is anything that needs one storage-facing design while giving consuming applications room to choose their own persistence stack. Sections - Framework authors - Multi-storage walkthrough - Auth libraries - Adapter ecosystem - Billing modules - Full-stack frameworks - Internal platforms If you want one concrete end-to-end example instead of a category page, start with the multi-storage walkthrough. 1. Auth-like libraries This is one of the clearest use cases. The usual problem Auth libraries often end up maintaining: - Prisma adapter logic - Drizzle adapter logic - app-specific schema examples - migration examples - docs for every storage story What Farming Labs ORM changes The auth package can define its schema once: Then the consuming app decides how to generate it: The package also gets one runtime-facing contract for storage helpers: 2. Billing and organization kits Billing modules often need reusable data definitions for: - plans - subscriptions - invoices - credits - seats - organizations - memberships Instead of hand-writing those tables and examples for every consumer stack, the package can ship one schema contract and let apps generate their preferred output. Example structure This is especially nice in a monorepo where many products need the same commercial model but not the same ORM choice. 3. Internal platform modules Platform teams often own cross-cutting packages for: - organizations - roles and permissions - audit logs - feature flags - provisioning state The cost of drift grows fast when every app re-implements that schema individually. With Farming Labs ORM, the platform team can: 1. own the shared schema package 2. publish docs and examples from the same contract 3. generate stack-specific output for each consumer app 4. Greenfield apps that want a safe starting point A greenfield app does not have to adopt a future runtime driver immediately. It can start with: - schema DSL - safe SQL generation - memory runtime for early tests and docs Then later add richer drivers without replacing the source of truth. 5. Multi-package monorepos The CLI supports multiple schema packages: That is one of the highest-leverage workflows in the project because it lets several reusable packages contribute models into one generated application surface. 6. When this approach is not the best fit Farming Labs ORM is probably overkill when: - you only have one app and one storage layer forever - your team is already happy with one ORM-specific schema as the sole source of truth - you do not need package-level reuse - you are not trying to publish or share a storage-facing contract Practical decision rule This approach becomes more valuable as these increase: - number of consumer apps - number of package boundaries - number of supported stacks - amount of duplicated storage docs and adapter work If your biggest pain is “we keep rewriting the same storage story for every consumer,” this project is aimed directly at that pain. --- ## Adapter Ecosystem URL: /docs/use-cases/auth-adapter-ecosystem How to replace repeated adapter packages with one schema definition, one unified ORM/runtime layer, and one setup path. Adapter Ecosystem Some libraries do not really want “an ORM.” What they actually need is a clean way to stop maintaining an adapter ecosystem. That is the problem this layer is aimed at. Libraries like NextAuth.js and Better Auth are good examples of packages that end up carrying a lot of repeated storage work: - one adapter package per backend - one schema example per backend - one setup story per backend - one error-parsing story per backend - one docs track per backend Farming Labs ORM gives you a way to collapse much of that into: - one schema definition - one ORM/runtime layer - one setup and bootstrap path - one capability surface - one normalized error boundary The point is not to add one more adapter. The point is to reduce how many adapter-shaped things the library has to own at all. What the replacement looks like The replacement pattern is simple: 1. define the storage contract once 2. generate backend-specific artifacts when the consumer wants generated output 3. create a unified ORM from the consumer's raw runtime client when the consumer wants live execution 4. write the package storage logic once against that unified ORM 5. use the same setup helpers in tests, demos, and framework-owned install flows That gives the package one storage-facing core instead of many backend-specific tracks. Step 1: define the storage contract once The package should own one schema definition instead of several ORM-specific versions of the same model. This pattern is useful for auth libraries, billing modules, platform packages, and framework-owned storage modules. The example happens to look auth-shaped because that is one of the clearest cases, but the same replacement pattern works anywhere a package owns a shared storage contract. Step 2: generate backend-specific output from the same schema If the consuming app wants generated Prisma, Drizzle, or SQL artifacts, the package does not need a separate storage definition. This is the generation-first path. Use it when the package wants to: - power an installer or CLI - emit framework starter files - snapshot generated output in tests - publish generated examples in docs Step 3: accept a raw runtime client when the app already has one If the consumer already owns a real database client, the package can normalize that client into one ORM layer instead of forcing the user to manually wire a different adapter package. If the package wants to explain integration failures more clearly before it constructs the ORM, it can inspect the runtime first: This is the runtime-first path. It is the path that makes “bring your own client” integration possible. That is why libraries like NextAuth.js and Better Auth tend to need this kind of solution: they want the app to keep control of the final database client while still letting the package own one shared storage layer. Step 4: write the package storage logic once Once the package has a unified ORM, the storage helpers only need to be written one time. This is the part that actually replaces the adapter ecosystem. The package no longer needs: - one Prisma query implementation - one Drizzle query implementation - one Kysely query implementation - one MongoDB query implementation - one SQL query implementation It keeps one storage implementation and lets the app choose how that implementation gets translated underneath. Step 5: use one setup path in tests, demos, and install flows The same package can also stop owning a separate setup story per backend. If the package just needs to prepare the database: If the package wants setup plus the ORM back: This matters a lot in: - integration tests - self-hosted install flows - framework starter kits - local demos - preview apps The shape of the replacement Without this pattern, a library ecosystem usually grows sideways: - more adapters - more setup docs - more migration docs - more backend-specific edge cases With this pattern, the library grows inward around a stable core: - one schema definition - one package-owned storage API - one runtime-normalized query layer - one capability surface - one normalized error surface That is the actual replacement story. The library still may need custom work for some platform-specific backends, but the shared contract stops fragmenting across every mainstream storage stack. Potential improvements this unlocks Once a package stops centering many backend-specific adapters, a few important improvements become much easier to ship. - a storage fix can land once instead of being repeated across several adapters - docs, starters, and examples can stay closer to the real package behavior - integration tests can validate one storage contract across many runtimes - capabilities such as numeric IDs, namespaces, transactions, and richer setup flows can be adopted in one shared layer - plugin and module ecosystems can depend on one stable storage surface instead of backend-specific assumptions - setup UX can get simpler because the package can reuse pushSchema(...), applySchema(...), and bootstrapDatabase(...) - error handling can get more consistent because the package can react to normalized ORM errors instead of parsing several backend-specific error formats That is usually where the biggest win shows up. The value is not only fewer adapters. It is also a cleaner place to improve correctness, docs, testing, and integration UX over time. Performance and platform benefits This replacement pattern can also improve the operational side of the package, not just the maintenance story. - bundle shape can get smaller because the package can center one shared storage layer instead of pulling several adapter implementations into the same surface area - serverless and edge-friendly integration gets easier because the runtime path can stay lighter and only load the backend-specific pieces the app actually uses - startup behavior can improve when the package avoids eagerly wiring many adapter branches or backend-specific helpers up front - test runtimes can get simpler because the same setup helpers and storage contract can be reused across the matrix instead of maintaining many custom harnesses - bug fixes that remove extra round trips or fallback logic can benefit every runtime that uses the shared layer instead of being repeated adapter by adapter The important nuance is that this does not magically make every database query faster on its own. Prisma is still Prisma. MongoDB is still MongoDB. Direct SQL is still limited by the underlying network and database cost. The more realistic wins usually come from: - less duplicated fix-up logic - fewer unnecessary adapter-specific branches - cleaner capability-driven behavior - better setup and bootstrap ergonomics - lighter import and runtime surfaces for packages that do not need every backend at once --- ## Auth Libraries URL: /docs/use-cases/auth-libraries How to build auth-style storage integrations on top of Farming Labs ORM. Auth Libraries Auth libraries are one of the strongest fits for Farming Labs ORM because they usually need to describe the same storage shape across many app stacks without rewriting the same adapter story over and over. For many auth packages, that means Farming Labs ORM can become the drop-in replacement for much of the adapter ecosystem they would otherwise have to own and document separately. What auth libraries usually need Most auth systems end up modeling the same ideas: - users - sessions - linked accounts - verification records - organizations and memberships - plugin-owned models The repeated work usually shows up in three places: 1. schema examples for each supported stack 2. adapter or storage code for each backend 3. docs that have to explain the same model in different ORM dialects Farming Labs ORM helps by letting the package define the storage contract once, then letting the consuming app choose the eventual runtime or generator target. Recommended integration model The cleanest auth-package architecture usually looks like this: 1. the auth package owns the schema contract 2. the app chooses how to generate or execute it 3. the auth package writes its storage helpers once against the unified runtime That means the package does not need separate query logic for Prisma, Drizzle, Kysely, raw SQL, and MongoDB. If you are evaluating the broader adapter surface that auth packages usually care about, see Adapter Ecosystem for the more general replacement pattern around one schema definition, one ORM/runtime layer, and one setup path instead of many adapter packages. Step 1: define the schema once The important part is that the auth package now owns the model once. Step 2: let the app generate what it needs If the consuming app wants Prisma output: If the app wants Drizzle or safe SQL instead, the schema package does not have to change. If the auth library wants to render artifacts in memory for a CLI, installer, or docs example, it can also call the generators directly: That is useful when the auth package wants to: - power an installer or setup wizard - emit artifacts in a framework CLI - snapshot generated output in tests - show docs examples that come from the real schema contract Step 3: write the auth storage helpers once The auth package can now write its data access once against the unified runtime surface. That gives the auth package one place to implement: - load a user by email, id, token, or linked account - rotate or revoke sessions - link provider accounts - count related records for hooks or plugin rules Step 4: accept raw clients when the integration needs to If your auth framework wants to accept a raw database client directly, use the runtime helpers instead of rebuilding runtime detection yourself. That keeps the auth integration small: 1. accept a raw client 2. normalize it into Farming ORM 3. run the shared auth storage helpers against the normalized client If the integration needs to debug what the app passed in before creating the ORM, inspect the runtime first: Step 5: use capabilities instead of guessing Higher-level auth libraries often need to know what the runtime can safely do. That is useful for decisions like: - whether numeric IDs are manual or generated - whether transactions should wrap multi-step account linking - whether a schema namespace is available on Postgres - whether a fallback path is needed for relation-heavy reads Step 6: use normalized errors at the boundary Auth systems often need to react consistently to duplicate emails or duplicate provider-account pairs. That is cleaner than parsing Prisma codes, SQLSTATEs, MySQL driver errors, and Mongo duplicate key errors separately in the auth package. Step 7: bootstrap in tests and local setup If the auth package or framework needs to stand up a real database in tests, use the runtime-aware setup helpers. That is especially useful in: - integration tests - local demos - framework-owned setup flows - preview apps If the auth package only needs to prepare the database and does not want the ORM returned yet, use: Use bootstrapDatabase(...) when you want setup plus the ORM client back. Use pushSchema(...) or applySchema(...) when the package already has its own ORM or wants setup as a separate step. Helper map for auth packages - renderPrismaSchema(...): emit schema.prisma text from the auth schema - renderDrizzleSchema(...): emit a Drizzle schema module for starter kits or generated output - renderSafeSql(...): emit SQL DDL for direct SQL installs or snapshot testing - inspectDatabaseRuntime(...): explain what raw client was passed to the auth integration - createOrmFromRuntime(...): turn a raw client into the unified auth runtime - pushSchema(...): prepare a real database before tests, examples, or setup flows - bootstrapDatabase(...): prepare the database and return an ORM in one step Numeric IDs and namespaces If the auth library needs numeric IDs, the schema can choose that explicitly: That is first-class on: - SQL - Drizzle - Kysely - Prisma - memory MongoDB and Mongoose currently support manual numeric IDs only. If the auth package needs Postgres namespaces, use: Do not pass flat strings like "auth.users". The ORM intentionally rejects that shape so namespaces stay explicit. Good auth-package rules of thumb - keep normalization helpers like normalizeEmail(...) shared between writes and reads - declare compound uniques such as provider + accountId in the schema, not only in app docs - keep backend-specific branching out of the auth package whenever possible - let the consuming app choose the generator/runtime target - use runtime capabilities and normalized errors instead of guessing backend behavior What this removes - duplicated storage code per ORM - drift between auth docs and auth runtime behavior - hand-maintained adapter logic for every consumer stack It does not remove every integration detail, but it centralizes the part that really is shared. --- ## Billing Modules URL: /docs/use-cases/billing-modules How reusable billing packages can ship one storage contract across many apps and runtime stacks. Billing Modules Billing packages are one of the clearest fits for Farming Labs ORM because the commercial model usually changes more slowly than the consuming app stack. The same billing package often needs to work across: - product applications - admin dashboards - background workers - webhook processors - self-hosted installs - starter kits - multiple ORM or database preferences That means billing teams often end up repeating the same storage contract in too many places. What billing packages usually need Most reusable billing systems eventually model some combination of: - plans - prices - subscriptions - customers - invoices - payment attempts - entitlements - usage events - seats - organization billing state The drift usually shows up in three places: 1. every stack gets its own schema flavor 2. every backend gets its own storage helpers 3. docs examples slowly stop matching the real billing model Farming Labs ORM helps by letting the billing package own the domain contract once while the consuming application still chooses the final runtime or generated output. Recommended billing-package architecture The cleanest pattern usually looks like this: 1. the billing package owns the billing schema 2. the app decides what it wants to generate or execute 3. the billing package writes its data helpers once against the unified runtime 4. tests, demos, and examples bootstrap the same schema through runtime helpers That means the billing package does not need separate Prisma-only, SQL-only, and Mongo-only business logic just to load subscriptions and issue invoices. Step 1: define the billing schema once This gives the package one durable business contract instead of asking every consumer to reinterpret billing tables in its own dialect. If a Postgres deployment wants a dedicated namespace, the package can also move those tables into billing.* later with tableName(...) without changing the rest of the billing model. Step 2: let the app choose its generation or runtime path If a consuming app wants Prisma artifacts: If another app wants Drizzle, safe SQL, or runtime-only execution, the billing package does not have to change its data model. That split is a big deal for reusable billing kits because billing packages often need to serve: - SaaS apps using Prisma - internal tools using Drizzle or Kysely - workers using direct SQL - self-hosted installs that want generated SQL artifacts If the package wants to render those artifacts directly in memory for a CLI, installer, or docs pipeline, it can use the generators: That is useful when the billing package needs to: - power installation flows - generate self-hosted setup artifacts - document the real billing schema in examples - snapshot generated billing output in tests Step 3: write billing helpers once against the unified runtime That gives the billing package one place to implement: - active-subscription reads - plan changes - invoice creation - entitlement management - usage recording - provider customer reconciliation without duplicating the logic per backend. Step 4: accept raw clients when the integration needs to If your billing package or commerce framework wants to accept a raw database client directly, it should lean on the runtime helpers instead of rebuilding runtime detection and driver creation itself. That gives the consumer a small integration surface: 1. pass the raw client 2. let Farming Labs ORM normalize it 3. run shared billing helpers against the normalized runtime If a consuming app passes an unexpected wrapper or proxy around the database client, inspect it first: Step 5: use capabilities for billing behavior Billing systems often need to branch on runtime behavior more carefully than a typical app. That can influence decisions like: - whether checkout and invoice writes should be wrapped in one transaction - whether generated numeric IDs are available - whether a dedicated billing.* Postgres namespace is usable - whether idempotent provider syncs should prefer native upsert or fallback logic - whether a post-write read is needed after invoice or subscription mutations Step 6: normalize storage failures at the boundary Billing systems usually need to convert backend failures into product-facing behavior. That keeps the billing package from learning Prisma, Postgres, MySQL, and Mongo error formats separately. Step 7: bootstrap billing databases in tests, demos, and webhooks Billing modules often need a real database in: - integration tests - example apps - webhook replay tooling - self-hosted setup flows - local development Use the runtime-aware setup helpers for that: That gives the package a single runtime-aware setup path instead of one Prisma-specific setup story, one SQL-specific story, and one Mongo-specific story. If the billing package wants schema setup as a distinct step, use pushSchema(...) or applySchema(...) directly: That is often useful for: - setup wizards - CLI commands - self-hosted installation flows - test harnesses that want setup before creating the final runtime Helper map for billing modules - renderPrismaSchema(...): emit Prisma artifacts for hosted apps or starter kits - renderDrizzleSchema(...): emit a Drizzle schema file for generated billing installs - renderSafeSql(...): emit SQL DDL for direct SQL or self-hosted setup flows - inspectDatabaseRuntime(...): inspect the raw billing runtime before normalizing it - createOrmFromRuntime(...): create the billing runtime from a raw client - pushSchema(...): apply the billing schema to a live database as a separate setup step - bootstrapDatabase(...): prepare the billing database and return the ORM in one call Good places for a billing package to use this - SaaS billing kits shared across many products - framework-owned billing modules - commerce engines that need to accept many runtimes - admin consoles and workers that share the same billing schema - starter kits that want one durable billing contract When this approach is less useful Farming Labs ORM is less compelling when a billing package: - only supports one stack forever - wants to be tightly coupled to one ORM-native schema language - never needs to publish or share a storage-facing billing contract Practical rule of thumb If your billing problem sounds like: - "we keep rewriting plan and subscription models per stack" - "our invoice docs drift from our actual schema" - "our workers and apps disagree on billing tables" - "we want one billing kit to work across many products" then Farming Labs ORM is a strong fit. --- ## Framework Authors URL: /docs/use-cases/framework-authors How frameworks and reusable platform layers can build one storage contract and let apps bring the final runtime. Framework Authors This guide is for people building a framework, platform layer, or reusable module system on top of Farming Labs ORM. That usually means the framework wants to own storage for things like: - auth state - billing state - cache entries - independent key-value records - rate limits - audit or event logs - observability snapshots - feature flags - framework-owned metadata The goal is not to make the framework into its own database product. The goal is to keep one durable storage contract while letting the consuming app bring Prisma, Drizzle, Kysely, MikroORM, TypeORM, Sequelize, Cloudflare D1, Cloudflare KV, Redis, Supabase, EdgeDB / Gel, direct SQL, MongoDB, Firestore, DynamoDB, Unstorage, or another supported runtime. The problem frameworks usually run into Without a unifying layer, framework-owned modules tend to drift into: - one Prisma implementation - one Drizzle implementation - one SQL-only setup path - one MongoDB implementation - one document-store implementation - duplicated install docs - duplicated error handling That is how a framework slowly turns into an adapter ecosystem instead of one storage layer. The intended integration path 1. Keep the framework schema in the package That single schema can later power: - runtime execution - generated Prisma output - generated Drizzle output - safe SQL output - setup/bootstrap helpers 2. Accept the raw client from the app The framework boundary should usually accept the app's real database or ORM client, not force the app to wrap it into another adapter shape first. That is what lets one framework runtime support many execution stacks without forking the framework storage logic into one implementation per backend. 3. Inspect the runtime when integration fails If the app passes a wrapped or unsupported client, inspect it before constructing the ORM: That gives the framework a much better onboarding story than a generic "unsupported client" failure. 4. Write the framework storage layer once This is the key value: - the framework writes the storage layer once - Farming Labs ORM handles the translation across the supported database and ORM stacks underneath 5. Use capabilities instead of backend guesses When a framework needs behavior decisions, inspect capabilities: That gives the framework a generic way to make runtime decisions without hard-coding Prisma-only, SQL-only, or Mongo-only assumptions. 6. Handle normalized errors at the framework boundary That keeps the framework from owning one parser for Prisma codes, another for SQLSTATEs, another for Mongo errors, and so on. 7. Use one setup path in tests, starters, and install flows If the framework wants to prepare the live database in tests, demos, starter kits, or install flows, use the setup helpers: That gives you: - one framework-owned install shape - one starter-kit setup shape - one demo/bootstrap shape - one test setup shape instead of one flow per backend family. Where this fits best This pattern is especially strong when the framework wants to behave like: - "bring your own database or ORM client" - "one storage contract, many execution stacks" - "one docs story and one setup story instead of many adapter stories" - "framework-owned modules without framework-owned adapter sprawl" That is why it fits reusable full-stack frameworks, platform layers, starter kits, and module systems so well. Best next pages - framework-scale examples: Full-Stack Frameworks - shared platform package examples: Internal Platforms - support coverage by stack: Support Matrix --- ## Full-Stack Frameworks URL: /docs/use-cases/fullstack-frameworks How full-stack frameworks can integrate Farming Labs ORM without owning every backend-specific storage branch. Full-Stack Frameworks Full-stack frameworks usually end up owning more storage-facing behavior than they planned. Even when the framework does not call itself an ORM or a database layer, it often still needs a durable place for: - auth and identity - teams, organizations, and permissions - billing and plan state - CMS-like content - route caches and render metadata - background job state - webhook logs and retries - feature flags and rollout state - environments, tenants, and project settings - starter templates and example apps The hard part is usually not query syntax. The hard part is having to explain, support, and maintain the same storage story across several different runtime stacks. Why frameworks drift so quickly Frameworks often end up owning a surprising number of storage surfaces at once: - framework core modules - official add-ons - starter kits - example applications - CLI setup flows - test fixtures - docs snippets If each surface assumes a different backend shape, the framework slowly drifts into: - Prisma-only assumptions in one package - Drizzle-specific examples in docs - raw SQL helpers in workers - duplicated migrations or setup stories - plugin modules that only work with one storage style Farming Labs ORM helps by letting the framework define and consume one storage-facing contract while still letting the application choose the final runtime. Where Farming Labs ORM fits inside a framework For framework authors, the most useful pattern is: 1. define the framework-owned schema contract once 2. accept a raw runtime client from the app 3. create the ORM from that runtime 4. expose framework modules that talk to the unified runtime 5. use runtime-aware setup helpers in tests, templates, and examples That keeps the framework focused on product behavior instead of backend plumbing. A practical framework architecture 1. Keep the schema in a shared package This does not mean the framework must own every model in one giant schema file. In practice, many frameworks will compose several schema packages: - auth schema - billing schema - organization schema - cache or job schema The important part is that the framework defines those contracts once instead of rewriting them per backend. If the framework also wants to render artifacts directly, it can call the generators from the same schema package: That is especially useful when the framework owns: - official starter kits - a setup CLI - example apps - documentation snippets - self-hosted installation artifacts 2. Accept the raw client at the framework boundary This is one of the most valuable integration points because the framework does not need to force the app to manually build the ORM first. The framework can simply say: 1. pass your raw Prisma, Drizzle, Kysely, MikroORM, SQL, Mongo, or Mongoose client 2. let the framework normalize it through Farming Labs ORM 3. use the framework modules on top of that normalized runtime If the framework wants to explain integration errors more clearly, inspect the runtime before constructing the ORM: 3. Write framework storage helpers once That keeps the framework code focused on framework behavior instead of backend plumbing. Concrete places a full-stack framework can use the ORM This is where the ORM tends to fit naturally. Framework auth package A framework can own: - users - sessions - linked accounts - organization membership while still accepting a raw Prisma, SQL, Mongo, or Drizzle runtime from the application. Framework billing package A framework can ship: - plans - subscriptions - invoices - usage events as one shared billing contract across starter apps and self-hosted deployments. Framework teams and permissions module Many frameworks eventually need: - projects - organizations - memberships - roles - feature ownership Those are classic shared-contract models that benefit from one schema source of truth. Framework route cache or render metadata module A framework that owns route output caching, prerender metadata, or invalidation state can keep one portable record shape instead of documenting it differently for every backend. Framework CMS or admin module If the framework wants to ship content, admin, or internal tooling modules, it can define content entities once while still letting each app choose where those records live. Framework jobs and workflows module If the framework ships background jobs or workflow state, a unified runtime surface is useful for: - job creation - status transitions - retries - dead-letter handling - operator dashboards Framework webhooks and integration logs Frameworks that coordinate external services usually need durable storage for: - delivery attempts - retry state - payload metadata - endpoint configuration Those records are a good fit for one shared framework contract. Framework feature flags and environments module If a framework owns environment variables, rollout flags, or preview-deployment state, those are exactly the kinds of cross-cutting records that drift when each backend gets its own interpretation. Framework starter kits and example apps Even if the framework does not expose all of its internal models to users, starter kits still benefit from one shared schema contract. That keeps: - docs snippets - demo apps - test fixtures - CLI templates closer to the real product surface. How full-stack frameworks should structure the integration The cleanest approach is usually to split responsibilities like this: Framework package responsibilities - own the reusable schema contracts - own the storage helpers for framework modules - own runtime inspection and capability-driven branching - own example and test bootstrap flows Application responsibilities - choose the raw database client - choose the final runtime stack - choose whether to generate artifacts or stay runtime-first - decide whether the framework schema is merged with app-owned schemas That separation keeps the framework portable without taking runtime choice away from the app. Runtime-first, generation-first, and hybrid integration There are three common patterns. Runtime-first Use this when the framework mostly wants a runtime contract and wants the app to provide a raw client directly. This is a great fit when: - the framework wants a small integration API - the app already owns its database client - the framework does not need to own migrations directly Generation-first Use this when the framework also wants to ship generated artifacts or starter projects. This is a great fit when: - the framework ships official app templates - the framework wants generated Prisma, Drizzle, or SQL output - the app wants to keep its own migration workflow Hybrid Many frameworks will want both: - generation for starter apps, docs, and templates - runtime helpers for direct integration APIs That is often the best long-term story for a full-stack framework because it supports: - framework-owned examples - application-owned runtime choice - package-level reuse - docs that stay aligned with the actual schema Setup flows in dev, tests, and templates Frameworks often need to bootstrap a real database for: - e2e suites - example apps - local dev servers - preview deployments - template validation in CI Use the runtime-aware setup helpers for that: That gives the framework one runtime-aware setup story instead of one setup path per backend family. If the framework wants setup without immediately returning the ORM client, it can split the flow: That is useful for: - CLI setup commands - starter-template initialization - test harness bootstrapping - self-hosted installation steps Helper map for framework authors - renderPrismaSchema(...): generate Prisma artifacts from framework-owned schemas - renderDrizzleSchema(...): generate Drizzle schema modules for starters or examples - renderSafeSql(...): generate direct SQL artifacts for setup and self-hosting - inspectDatabaseRuntime(...): inspect what raw client the app actually passed - createOrmFromRuntime(...): normalize the app's runtime into the framework ORM - pushSchema(...): set up framework-owned tables before app startup or tests - bootstrapDatabase(...): set up the database and return the framework ORM in one step Capabilities matter for framework authors Frameworks usually need to branch on behavior more carefully than app code. That helps a framework choose: - whether generated numeric IDs are available - whether Postgres schema namespaces are usable - whether a transaction-backed flow is safe - whether relation-heavy framework helpers need a fallback path - whether setup/bootstrap should be exposed publicly or kept framework-owned - whether a given runtime is suitable for a specific framework module Error handling at the framework boundary Frameworks should usually convert ORM failures into framework-facing errors at the integration boundary. That keeps the framework from learning every backend's native error format and lets it expose stable framework-facing error semantics instead. When this is especially valuable Farming Labs ORM is especially useful for frameworks that: - ship official modules across several backends - maintain starter kits or example apps - want to support both hosted and self-hosted installs - own storage-facing product features, not just UI helpers - want one documentation story for many runtime choices When this is less useful Farming Labs ORM is less compelling when a framework: - only supports one backend forever - wants to stay deeply tied to one ORM-native schema language - never needs to share storage-facing contracts across packages or templates Practical rule of thumb If your framework pain sounds like: - "we keep rewriting the same storage docs" - "we keep branching on backend details in core code" - "our starter templates drift across stacks" - "our platform modules assume one database wrapper" then Farming Labs ORM is a strong fit. --- ## Internal Platforms URL: /docs/use-cases/internal-platforms Shared schema packages for organizations, permissions, and platform modules. Internal Platforms Internal platform teams often own models that many product teams consume. These packages are a strong fit for Farming Labs ORM because the platform team usually wants one durable contract while product teams still want freedom over their final runtime stack. Common platform-owned domains - organizations - memberships - roles - permissions - audit logs - feature flags - environment metadata Why a shared schema helps Without a shared contract, every product team tends to drift: - different table names - different field names - different docs examples - different migration patterns Platform packages often have to support: - generated artifacts for app teams - runtime helpers for internal services - setup flows for demos, tests, and sandboxes That is where the generator and runtime helper story becomes important. Platform package example Multi-package generation If a platform team wants to render artifacts directly instead of going through the CLI, it can call the generators from the shared schema package: If a platform-owned service wants to accept a raw client directly: If the platform package needs to prepare a database in tests, demos, or setup flows: Helper map for internal platforms - renderPrismaSchema(...): ship Prisma artifacts to product teams - renderDrizzleSchema(...): ship Drizzle schema modules from the shared platform contract - renderSafeSql(...): emit SQL setup artifacts for direct SQL consumers - createOrmFromRuntime(...): let platform services accept raw clients directly - pushSchema(...): prepare test, sandbox, or setup databases for platform-owned modules - bootstrapDatabase(...): prepare a live database and return the ORM in one step Why platform teams care - one place to evolve shared models - one set of docs examples - fewer app-specific reinterpretations - cleaner onboarding for product teams This is one of the highest-leverage uses of the project in a monorepo. --- ## Multi-Storage Walkthrough URL: /docs/use-cases/multi-storage-walkthrough Build one reusable platform layer that keeps durable state in a relational runtime and fast state in a key-value runtime. Multi-Storage Walkthrough If you want one concrete mental model for Farming Labs ORM, build a small platform layer that owns: - durable control-plane data such as users, workspaces, memberships, subscriptions, and audit logs - fast state such as sessions, rate limits, and cache entries That is a strong fit for Farming Labs ORM because the package can keep one schema-first storage contract while letting each app choose the final runtime pairing. In this walkthrough, the package will support both of these deployments: - a Node app using Prisma for durable state and Redis for fast state - a Cloudflare app using D1 for durable state and KV for fast state The important part is that the package storage code does not fork into a Prisma version, a D1 version, a Redis version, and a KV version. The package keeps one storage contract and lets the runtime layer translate it. 1. Split the problem by storage behavior The first design choice is not "which ORM should I support first?" It is: - which data needs durable relational behavior - which data is better treated as fast key-value state For this example: - the control plane uses a relational runtime - the fast state layer uses a key-value runtime That gives the package a clean boundary instead of trying to stretch one backend across every concern. 2. Define the durable schema Put the long-lived relational models into one shared schema package. This is the part of the package that usually wants: - relational lookups - compound uniques - stronger transaction behavior - readable generated artifacts such as Prisma or SQL output 3. Define the fast-state schema Put short-lived and key-value-friendly state into a separate schema. This side of the package is a good fit for Redis, Upstash Redis, Cloudflare KV, or Unstorage-style runtimes. The point is not to turn those backends into a full relational database. The point is to keep one schema and one query API for state that already fits key-value behavior. 4. Generate artifacts for the durable side The durable schema is usually the one that benefits most from generated artifacts. That keeps the package friendly for apps that want normal Prisma or SQL-first setup flows, while the fast-state schema can stay runtime-first. 5. Accept the raw runtime clients The package boundary should accept the app's real clients and normalize them through the runtime helpers. That one boundary is what lets the same package support: - Prisma + Redis - TypeORM + Upstash Redis - direct SQL + Unstorage - D1 + KV without rewriting the storage layer for every stack pairing. 6. Write the storage helpers once Now the package can compose both runtimes behind one platform-facing API. This is the most important part of the walkthrough. The storage helpers do not know whether the durable client is Prisma, D1, TypeORM, or direct SQL. They also do not know whether the fast client is Redis, KV, or Unstorage. They only know the schema contract and the unified ORM surface. 7. Wire it into a Node app Here is the same package running in a normal Node deployment with Prisma and Redis. In this deployment: - Prisma owns the durable relational state - Redis owns the fast session, cache, and rate-limit state - the package storage helpers stay unchanged 8. Wire the same package into a Cloudflare app Now take the same package and run it in a Cloudflare-shaped deployment. The package code does not gain a "Cloudflare adapter" branch here. It still: - accepts raw runtime clients - normalizes them through the runtime layer - reuses the same storage helpers That is the core portability story Farming Labs ORM is trying to unlock. 9. Use one setup path in tests and local demos If the package also owns examples, starter apps, or integration tests, use the setup helpers instead of maintaining one setup branch per backend. That gives the package one entrypoint for: - tests - examples - starter kits - onboarding scripts On SQL-style runtimes, bootstrapDatabase(...) can prepare the database. On Redis, KV, and other runtime-first backends, it becomes the same safe no-op setup surface instead of forcing a separate branch. 10. Design rules that keep this healthy The pattern works best when the package stays honest about runtime boundaries. Keep durable relational data on the durable side Put things like these in the relational runtime: - users - workspaces - memberships - subscriptions - audit logs Those models usually want better relational planning, stronger consistency, and more readable generated artifacts. Keep fast state on the fast side Put things like these in the key-value side: - sessions - rate limits - cache entries - temporary verification or provisioning state That lets the app choose Redis, KV, or another fast runtime without trying to make it own the whole control plane. Do not pretend this is one cross-store transaction This pattern gives you one storage layer, not a distributed transaction coordinator. The package should still treat the durable and fast runtimes as separate systems: - write durable records first - populate fast state second - make cache and rate-limit data replaceable - avoid pretending both stores roll back together 11. What you get out of this At the end of this walkthrough, the package owns: - one durable schema - one fast-state schema - one runtime helper boundary - one package-level storage API And the consuming app still chooses the concrete pairing: - Prisma + Redis - D1 + KV - TypeORM + Upstash Redis - direct SQL + Unstorage That is the practical value of Farming Labs ORM for shared modules and framework-owned storage. Related guides - Framework authors - Full-stack frameworks - Runtime helpers - Prisma - Redis - Cloudflare D1 - Cloudflare KV ---