npm.io
2.5.6 • Published 2d agoCLI

forge-orm

Licence
MIT
Version
2.5.6
Deps
0
Size
1.7 MB
Vulns
0
Weekly
1.6K

forge-orm

A small, Prisma-shaped data layer for MongoDB, PostgreSQL, MySQL, SQLite, DuckDB and SQL Server. You write your models once in plain TypeScript and the same query code runs against any of the six databases. There is no code generation step, no Rust query engine, and no framework to adopt — just readable TypeScript over the official drivers, organised one adapter per database.

npm install forge-orm
import { createDb, f, model } from 'forge-orm';

const User = model('users', {
  id:    f.id(),
  email: f.string().unique(),
  name:  f.string(),
});

const db = await createDb({ url: process.env.DATABASE_URL!, schema: { user: User } });

const alice = await db.user.create({ data: { email: 'a@x.co', name: 'Alice' } }); // no id needed
const users = await db.user.findMany({ where: { name: { contains: 'Ali' } }, take: 10 });

The same code works whether DATABASE_URL is a Postgres, MySQL, SQLite, DuckDB, SQL Server, or Mongo connection string. forge picks the right driver from the URL prefix (postgres:, mysql:, sqlite:, duckdb:, mssql:, mongodb:).

Beyond the basics, forge ships first-class typed support for the things you usually have to drop to raw SQL for:

  • Geof.geoPoint() + near / nearTo / withinPolygon, compiling to PostGIS / MySQL spatial / SpatiaLite / DuckDB spatial / MSSQL GEOGRAPHY / Mongo 2dsphere. App-side Haversine fallback when no spatial extension is installed.
  • Vector similarityf.vector(1536, { metric: 'cosine' }) + the same near / nearTo vocabulary, compiling to pgvector / DuckDB vss HNSW / MSSQL VECTOR_DISTANCE / MySQL 9 DISTANCE / sqlite-vec / Mongo Atlas $vectorSearch.
  • JSON path querieswhere: { meta: { path: 'profile.age', gte: 18 } } on any f.json() / f.embed() / f.embedMany() / array column, compiling to PG ->/->>, MySQL JSON_EXTRACT, SQLite / DuckDB json_extract, MSSQL JSON_VALUE, Mongo dotted-key form.
  • Full-text searchf.text().searchable() builds the right index per dialect (Postgres GIN tsvector, MySQL FULLTEXT, SQLite FTS5 with shadow-table triggers, Mongo text, DuckDB fts) and the search operator queries it.

Sandbox / runnable examples

Try forge-orm without installing anything — every example is one click away on StackBlitz:

Browser-runnable One-liner
SQLite browser todo OPFS-persisted todo app, no server
Offline-first with sync outbox Optimistic writes + drain loop
DuckDB analytics 5K-row aggregation in a tab
Node + SQLite CLI Smallest possible forge-orm program
Hono + PGlite REST API Backend API, zero external DB
Next.js + PGlite full-stack App Router + Server Actions
Bun + SQLite blog Bun's built-in SQLite, zero deps
Feature deep-dives Pattern
Geo search geoPoint + nearTo (PostGIS / Mongo / wasm fallback)
Vector RAG f.vector(N) + cosine similarity
Recipe / BOM Recursive sub-recipe rollup
Multi-tenant scoping Soft RLS via app-layer wrapper
Audit log db.$events.on("mutation", …)
Full-text search .search() index across dialects
Transactions Atomic batch + nested savepoints
Migrations + drift db.$migrate() + db.$diff()
Real DB (point at your own) Setup
MongoDB Atlas blog Free Atlas tier, paste URI into .env
MSSQL ERP / MERGE Docker SQL Server, .env
Postgres + RLS auth Hard multi-tenant via current_setting

All 18 examples live at johnsonfash/forge-orm-examples. Clone any folder:

npx degit johnsonfash/forge-orm-examples/01-sqlite-browser-todo my-app

Contents

Deep-dive companions (docs/)

The README is the surface reference. For more depth — extra examples, edge cases, integration patterns — each major surface has its own companion doc. Eighty files in total, ~80,000 lines of reference material.

Schema and data model

Topic File
Model definition — full field catalogue, id strategies, enums, views, generated columns, schema namespacing, 5 worked schemas docs/MODEL.md
Embeds — f.embed/f.embedMany/f.json, indexing into embeds, Mongo $elemMatch, JSON-null markers, shape migration, 5 worked patterns docs/EMBED.md
Relations — one/many/inverse/cascade, join tables, polymorphic, self-ref, deep includes, 6 worked patterns docs/RELATIONS.md
Indexes — every IndexDef field, partial-filter, expression, INCLUDE, method matrix, drift detection, 6 worked patterns docs/INDEXES.md
Type safety — Row, every Infer* helper, ForgeOf / ForgeModels, autocomplete tricks, generics, 5 worked patterns docs/TYPES.md
Primary keys — UUIDv4 vs v7 vs ULID vs Snowflake vs serial, fragmentation, per-dialect emit, migration docs/PRIMARY-KEYS.md
Foreign keys — REFERENCES emit, onDelete/onUpdate, deferred checking, composite, online add, per-dialect quirks docs/FOREIGN-KEYS.md
Enums — f.enum(...), per-dialect emit, evolution (adding values online, expand/contract), lookup-table alternative docs/ENUMS.md
CHECK constraints — DB-enforced row invariants, NOT VALID+VALIDATE, NULL semantics, vs zod, Mongo $jsonSchema docs/CHECKS.md
Generated columns — STORED vs VIRTUAL, indexable JSON extracts, per-dialect matrix, common patterns docs/GENERATED-COLUMNS.md
Views — CREATE VIEW, updatable rules, SECURITY_BARRIER, indexed views, Mongo collection views docs/VIEWS.md
Materialized views — refresh strategies, CONCURRENTLY, MSSQL indexed views, Mongo $merge/$out, MySQL/SQLite emulation docs/MATERIALIZED-VIEWS.md
Triggers — DB-side procedural code, audit-log trigger, per-dialect model, Mongo change-stream alternative docs/TRIGGERS.md

Reads, writes, transactions

Topic File
Queries — every operator with per-dialect SQL/Mongo emit, cursor pagination, distinct, streaming, common bugs, 8 worked queries docs/QUERIES.md
Mutations — create/update/upsert/delete asymmetry, atomic ops, nested writes, idempotency, optimistic+pessimistic concurrency, 8 worked patterns docs/MUTATIONS.md
Transactions — callback vs array, per-dialect mechanics, savepoints, isolation, deadlock retry, Mongo replica-set, outbox, 5 worked patterns docs/TRANSACTIONS.md
Raw SQL — forgeSql composition, identifier-vs-value safety, per-dialect placeholders, $runCommandRaw, per-dialect worked patterns docs/RAW-SQL.md
Upsert — ON CONFLICT / ON DUPLICATE KEY / MERGE / findOneAndUpdate per dialect, partial updates, race semantics docs/UPSERT.md
Batch ops — createMany/updateMany/deleteMany, bind-parameter limits, chunking, ordered vs unordered, RETURNING docs/BATCH.md
Aggregations — count/sum/avg/groupBy/having/distinct, per-dialect emit, decimal precision, dashboard patterns docs/AGGREGATIONS.md
Window functions — ROW_NUMBER/RANK/LAG/LEAD/SUM OVER, per-dialect matrix, top-N per group, moving averages, sessionization docs/WINDOWS.md
Pagination — cursor / offset / numbered / infinite-scroll, total-count strategies, Relay/REST shapes, per-dialect docs/PAGINATION.md
Streaming — findManyStream per-driver internals, memory profile, backpressure, HTTP streaming, transactions+streaming docs/STREAMING.md
Locking — SELECT FOR UPDATE, advisory locks, SKIP LOCKED work queues, NOWAIT, deadlock prevention docs/LOCKING.md
Concurrency control — optimistic version columns, ETag/If-Match, retry strategies, per-dialect isolation quirks docs/CONCURRENCY.md

Cross-cutting surfaces

Topic File
Full-text search — every dialect's FTS engine, ranking, multi-column, languages, hybrid BM25+vector, highlighting, 6 worked patterns docs/FTS.md
Geo — SRIDs, dialect matrix, PostGIS, distance models, 3D, MultiPolygon, GeoJSON, spatial joins, H3, realtime tracking docs/GEO.md
Vector / embeddings / RAG — dialect picker, pipeline, hybrid BM25, versioning, HNSW/IVFFlat, quantization, multi-modal, eval docs/VECTOR.md
JSON path queries — per-dialect emit, indexing, migration, operator matrix, null markers, audit/webhook patterns, common bugs docs/JSON-PATH.md
Migrations — push-style model, every CLI flag, drift rules, per-dialect emit, three CI snippets, blue/green, monorepo, runtime split docs/MIGRATIONS.md
Drivers — bring-your-own-driver pattern, every shipped wrapper, six worked wrappers (Neon, Turso, D1, Atlas Data API, Bun, decorator) docs/DRIVERS.md

CLI and operations

Topic File
CLI reference — every forge subcommand and flag, exit codes, env config, CI snippets, programmatic equivalents docs/CLI.md
forge push — push semantics, --enable-extensions, --fallback, idempotency, dry-run, per-dialect DDL ordering docs/PUSH.md
forge diff — drift detection rules, DriftItem taxonomy, drift apply, per-dialect quirks, CI gating, the 2.5.1 auto-apply pass docs/DIFF.md
forge doctor — live capability probe, per-dialect checks, fix recipes, browserDoctor, K8s readinessProbe docs/DOCTOR.md
Rollback — snapshot rollback, forward-only, blue/green, per-dialect destructive-DDL limits, emergency restore docs/ROLLBACK.md
Seeding — idempotent upserts, bootstrap/dev/demo split, large seeds, faker, deterministic random, 3 worked seed programs docs/SEED.md
Deployment — env-per-stage, zero-downtime patterns, blue/green, containerized vs serverless, RDS Proxy, multi-region docs/DEPLOYMENT.md
Backup and restore — per-dialect primitives (pg_dump, MariaBackup, litestream, Atlas snapshots), PITR, restore drills, encryption docs/BACKUP-RESTORE.md
Schema versioning — additive-only rules, expand/contract for breaking changes, multi-app coordination, snapshot diffs docs/VERSIONING.md

Per-dialect deep dives

Topic File
PostgreSQL — pg/postgres.js/Neon HTTP matrix, type round-trip, JSONB, arrays, extensions, CONCURRENTLY, RLS, pgbouncer caveats docs/POSTGRES.md
MySQL / MariaDB — mysql2/mariadb drivers, 5.7 vs 8.x matrix, charset, FULLTEXT, native spatial, online DDL, replication docs/MYSQL.md
SQLite (server) — better-sqlite3/libsql/bun:sqlite, PRAGMAs, WAL, ALTER TABLE limits, extensions, litestream, sharding docs/SQLITE.md
MongoDB — relational-to-document mapping, index types, Atlas Search/Vector, change streams, transactions, sharding docs/MONGO.md
DuckDB — analytical workloads, Parquet/CSV ingestion, S3 httpfs, ATTACH cross-DB joins, EXPORT DATABASE, window funcs docs/DUCKDB.md
SQL Server — MERGE upsert, snapshot isolation, geography vs geometry, OPENJSON, Azure SQL specifics docs/MSSQL.md

Observability and errors

Topic File
Events — QueryEvent shape, semanticOp, subscribers, Pino/Sentry/OTel/Prometheus integrations, custom sinks docs/EVENTS.md
Logging — Pino/Winston/Bunyan wiring, redaction, sampling, request correlation, rotation docs/LOGGING.md
Tracing — OpenTelemetry SDK, span propagation, W3C traceparent, exporters (Jaeger, Tempo, Honeycomb, DataDog) docs/TRACING.md
Metrics — Prometheus, histogram buckets, cardinality discipline, RED/USE dashboards, alerting rules docs/METRICS.md
Errors — every error class, per-dialect code mapping, retry classes, backoff, Sentry/Bugsnag wiring docs/ERRORS.md

Performance

Topic File
Connection pooling — sizing per dialect, per-runtime constraints (Lambda, Workers, Bun), pgbouncer/RDS Proxy caveats docs/POOLING.md
Benchmarks — forge:bench methodology, scenarios, Prisma/Drizzle compare mode, profiling, CI regression gating docs/BENCHMARKS.md
Caching — DataLoader per-request, Redis cache-aside, CDN headers, event-driven invalidation, stampede prevention docs/CACHING.md
Preventing N+1 — include vs DataLoader, GraphQL resolvers, detection via event hook, per-dialect cross-product gotchas docs/N-PLUS-ONE.md

Patterns

Topic File
Soft delete — softDelete/restore, partial-filter uniques, query defaults, retention purge, GDPR caveats docs/SOFT-DELETE.md
Audit log — three shapes (single table, per-model history, append-only event), actor capture, hash-chain tamper resistance docs/AUDIT-LOG.md
Multi-tenant — shared-schema/schema-per-tenant/DB-per-tenant trade-offs, scopedDb, RLS, per-tenant migration orchestration docs/MULTI-TENANT.md
Sharding — shard-key choice, routing, cross-shard query patterns, resharding, native helpers (Vitess, Citus, Mongo) docs/SHARDING.md
Idempotency keys — Stripe-style model, atomic upsert primitive, TTL, webhook receivers, BullMQ jobIds, saga compensation docs/IDEMPOTENCY.md
Watch / change feeds — Mongo change streams, Postgres LISTEN/NOTIFY + logical replication, MySQL binlog, WebSocket fan-out docs/WATCH.md

Testing

Topic File
Testing — in-memory better-sqlite3, FakeWorker for browser, transaction-rollback reset, event-hook assertions docs/TESTING.md
Integration testing — testcontainers, Docker Compose, forge:integration:*, parallel-safe schema reset, GH Actions matrix docs/INTEGRATION-TESTING.md
Fixtures and factories — static fixtures, typed factories, seeded random, snapshot fixtures, browser OPFS fixtures docs/FIXTURES.md

Security

Topic File
Security — parameterized queries, RLS, column masking, field-level encryption, audit log, GDPR/HIPAA/PCI patterns docs/SECURITY.md
Encryption — at-rest (TDE, native), in-transit (TLS/mTLS), field-level (AES-GCM, CSFLE), KMS-backed key rotation docs/ENCRYPTION.md
SQLCipher — encrypted SQLite, driver matrix, key derivation, rekey, mobile lock-screen patterns docs/SQLCIPHER.md
Database auth — password vs IAM vs mTLS vs SSH-tunnel, IAM token refresh, secret rotation, RDS Proxy / Cloud SQL Auth docs/AUTH.md

Type-level reference

Topic File
Runtime validation (zod) — boundary parsing, two-schema asymmetry, transforms, OpenAPI gen, form-lib resolvers docs/RUNTIME-VALIDATION.md
Brand types — nominal IDs, zod .brand, multi-brand unions, money/time invariants, FK propagation docs/BRAND-TYPES.md
Dates and times — f.dateTime/f.date/f.time per-dialect emit, timezone strategy, Temporal API, DST/calendar pitfalls docs/DATES.md
Decimal and money — f.decimal({ precision, scale }), integer-cents pattern, dinero.js, per-dialect precision quirks docs/DECIMAL.md
UUID / ULID / Snowflake — bit layouts, DB-side generators, fragmentation, sortability, ObjectId, choice flowchart docs/UUID.md

Runtime targets

Topic File
Backend — server integration (hyper-express, Fastify, NestJS, Bun+Hono, pools, tx, BullMQ, multi-tenant, replicas, OTel, health, CI) docs/BACKEND.md
Browser — full sqlite-wasm + OPFS reference (URL schemes, worker, bundlers, $migrate, browserDoctor, ITP, multi-tab, pro build) docs/BROWSER.md
Browser frameworks — React+Vite, Next.js, Vue, Nuxt, SvelteKit, Angular, SolidStart, Astro, Remix, React Native, Tauri (11 recipes) docs/BROWSER-FRAMEWORKS.md
React — hooks, TanStack Query, Suspense, server vs client components, optimistic updates, sync, code-splitting, testing, 6 worked patterns docs/REACT.md
Mobile — RN bare, Expo, Capacitor, Tauri, SQLCipher, sync patterns, background tasks, testing, migration cookbooks docs/MOBILE.md
Cloudflare Workers / Vercel Edge — V8 isolate constraints, D1, Hyperdrive-fronted Postgres, Neon HTTP, Turso, cache patterns docs/WORKERS.md
AWS Lambda — handler-scope pool, RDS Proxy, Aurora Serverless Data API, IAM token refresh, SIGTERM drain, cold-start budget docs/LAMBDA.md

What forge is, and what it is not

forge is a thin wrapper. It turns a Prisma-style call such as db.user.findMany({ where: { active: true } }) into the right query for your database and runs it through the official driver (pg, mysql2, better-sqlite3, mongodb, @duckdb/node-api, mssql). The drivers do the actual work; forge builds the queries and shapes the results.

Reach for forge when you want one query API across more than one database, a dependency small enough to read and fork, full TypeScript autocomplete with no generated client to keep in sync, and the option to drop down to raw SQL at any time.

forge is not a replacement for Prisma or Drizzle in maturity. It has fewer features, a smaller ecosystem, and no GUI. If you need those, use Prisma or Drizzle. The honest notes at the end spell this out.

What's new

Full release history is in CHANGELOG.md. Recent highlights:

  • 2.5 — MSSQL MERGE upsert, Mongo cross-field nearTo, browser $doctor/$diff, MultiPolygon + GeometryCollection, 3D / Z coordinates, non-WGS84 SRIDs. Closes the entire "Coming soon" list from 2.4. MSSQL upsert now compiles to a proper MERGE INTO … USING (VALUES) … WHEN MATCHED THEN UPDATE … WHEN NOT MATCHED THEN INSERT OUTPUT inserted.* (atomic, returns the row). Mongo near filter on field A + nearTo orderBy on field B now both fire — cross-field rewrite emits $geoWithin: { $centerSphere } for A inside the $geoNear.query so the single-stage limit doesn't drop A. db.$doctor() and db.$diff() are the browser equivalents of the forge doctor / forge diff CLIs — returns structured reports your app can render. withinPolygon accepts Polygon-with-holes, MultiPolygon, and GeometryCollection (normalised through every dialect's WKT and the fallback ray-cast — holes correctly excluded via even-odd rule). f.geoPoint({ dims: 3 }) opts into XYZ storage (PG geography(PointZ), SQLite POINT Z, DuckDB ST_Point3D, MSSQL POINT(x y z)); distance ops remain ground-distance (2D-on-sphere) — altitude round-trips. f.geoPoint({ srid: 3857 }) (or any non-4326) routes PG to geometry(Point, srid) (geography is 4326-only); MySQL / SQLite / DuckDB / MSSQL accept the declared SRID at DDL time. Coordinates are user-provided in the target SRID — no auto-reprojection.
  • 2.4 — Browser adapter: sqlite-wasm + OPFS, runtime $migrate, bundler plugins. Real SQLite in the browser via @sqlite.org/sqlite-wasm running in a Web Worker, persisted on the Origin Private File System. New URL schemes opfs:, opfs-sahpool:, and :memory:; a new wasmSqliteDriver() factory; ready-to-import bundler plugins for Vite (forge-orm/wasm/vite), Next.js (forge-orm/wasm/next), and Webpack 5 (forge-orm/wasm/webpack); db.$migrate() runtime DDL apply (the browser replacement for forge push); browserDoctor() feature-detection (OPFS, FTS5, R-Tree, sqlite-vec, persistent storage); and an opt-in custom wasm build path (forge-orm/wasm/worker-pro) with R-Tree + sqlite-vec compiled in for native geo + vector search. See Browser (sqlite-wasm + OPFS).
  • 2.3 — DuckDB + MSSQL adapters, end-to-end geo, JSON path queries, vector search. Two new dialects (duckdb: and mssql: URL prefixes); typed geo (f.geoPoint() + near / nearTo / withinPolygon across all 6 dialects, plus a fallback mode for envs without the spatial extension); typed JSON path reads (where: { meta: { path: 'profile.age', gte: 18 } }); typed vector similarity (f.vector(1536, { metric: 'cosine' }) + the same near / nearTo vocabulary, compiling to pgvector / DuckDB vss / MSSQL VECTOR_DISTANCE / Mongo $vectorSearch); forge doctor live capability probe; forge push --enable-extensions; a throwaway driver smoke harness (npm run smoke:drivers).
  • 2.2 — IndexDef covers the shapes forge push couldn't model. SQL partial indexes (where: 'deleted_at IS NULL'), expression indexes (expression: 'lower(email)'), Postgres access methods (gin / gist / brin / hash) plus INCLUDE covering columns, MySQL FULLTEXT parser plugins / invisible indexes / multi-valued JSON indexes, and Mongo geospatial ('2dsphere' / '2d'), hashed shard keys, collation, and wildcard projection.
  • 2.1 — partial indexes on MongoDB. A schema IndexDef now accepts partialFilterExpression, so forge push can build a partial index — e.g. a unique index that only covers documents where the field is a string.
  • 2.0 — delete() is always a hard delete. Breaking change: delete() / deleteMany() permanently remove rows on every model; the recoverable path is the explicit softDelete() / restore() verbs. See Soft delete.
  • 1.9 — pluggable MySQL + Mongo. MySQL adds mariadbDriver and planetscaleDriver alongside the default mysql2; Mongo lets you bring your own MongoClient (mongoDriver) for DocumentDB / Cosmos / FerretDB / custom options.
  • 1.8 — pluggable Postgres drivers. Use postgres.js (porsager) instead of node-postgres, or any client you wrap, via createDb({ driver: postgresJsDriver(...) }).
  • 1.7 — pluggable SQLite drivers. Run forge in React Native (expo-sqlite, op-sqlite), on the edge / Turso (libsql), or over any driver you wrap.
  • 1.6 — richer aggregates. groupBy's having accepts both Prisma's field-first shape and the bucket-first shape; count({ distinct: [...] }) is fixed on MongoDB.
  • 1.5 — col() for field-to-field comparison. Compare one column against another inside a where ({ currentUsage: { lt: col('globalLimit') } }), portable across every dialect.
  • 1.4 — primary-key strategies on f.id() (auto / uuid / bigserial).

Install and pick your driver

forge ships no database driver of its own. You install only the driver for the database you use. Each one is an optional peer dependency, so npm install forge-orm on its own pulls nothing extra, and importing forge needs no driver at all.

Database Connection string starts with Install
PostgreSQL postgres:// or postgresql:// npm install pg
MySQL or MariaDB mysql:// npm install mysql2
SQLite sqlite: or file: npm install better-sqlite3
MongoDB mongodb:// or mongodb+srv:// npm install mongodb
DuckDB duckdb: npm install @duckdb/node-api
SQL Server (MSSQL) mssql: or sqlserver: npm install mssql
Browser (SQLite) opfs:, opfs-sahpool:, :memory: npm install @sqlite.org/sqlite-wasm
npm install forge-orm      # the library, no drivers
npm install pg             # add the one you need

The driver loads lazily, the first time you actually run a query against that database. Importing forge, defining a schema, or using one database never needs the other databases' drivers installed. If a driver is missing when you connect, you get a clear message telling you what to install rather than a crash.

There is no lock-in. No generated client to regenerate, no migration state you cannot leave, no framework module to wire in, and no driver bundled inside. It is plain TypeScript over the official drivers, and you can always call the driver directly if you outgrow it.

See more — docs/DRIVERS.md for the bring-your-own-driver pattern, every shipped wrapper, six worked wrappers (Neon HTTP, Turso, Cloudflare D1, Atlas Data API, Bun:sqlite, logging decorator), capability flags, and per-driver perf notes.


Connecting

createDb takes a connection URL and your schema. It returns a typed db handle whose properties match your model names.

import { createDb } from 'forge-orm';

const db = await createDb({
  url: process.env.DATABASE_URL!,   // postgres://… | mysql://… | sqlite:… | duckdb:… | mssql:… | mongodb://…
  schema: { user: User, post: Post },
});

// later, when shutting down:
await db.$disconnect();

Options:

  • url is the connection string. The prefix selects the database.
  • schema is your model map. db.<key> exists for each key (for example db.user, db.post).
  • type (optional) forces the database type if the URL is ambiguous: 'postgres' | 'mysql' | 'sqlite' | 'mongo' | 'duckdb' | 'mssql'.
  • strict (optional, default false). When true, a query that filters on an unknown field name throws instead of silently matching nothing. Useful for catching typos.

You can also pass connection parts instead of a URL:

await createDb({ type: 'postgres', host: 'localhost', database: 'app', user: 'me', schema });
Pluggable drivers

All six databases ship a sensible default driver, and all six let you swap in another client — for React Native, edge / serverless runtimes, or a managed / API-compatible backend. Instead of a URL you open the client yourself (you own its config and lifecycle), wrap it with one of forge's driver factories, and pass it as driver. The query API is identical whichever client backs it.

const db = await createDb({ schema, driver: someDriver(client) });   // no url needed

Built-in drivers:

Database Default driver Built-in alternatives
SQLite betterSqlite3Driver (better-sqlite3) expoSqliteDriver (Expo/RN), opSqliteDriver (bare RN), libsqlDriver (libsql/Turso/edge), wasmSqliteDriver (browser + OPFS)
Postgres pgDriver (pg) postgresJsDriver (postgres.js)
MySQL mysql2Driver (mysql2) mariadbDriver (MariaDB connector), planetscaleDriver (@planetscale/database)
MongoDB built-in mongodb client mongoDriver(client) — your own MongoClient (DocumentDB, Cosmos, FerretDB, custom)
DuckDB duckdbDriver (@duckdb/node-api)
MSSQL mssqlDriver (mssql)
// SQLite on Expo / React Native
import * as SQLite from 'expo-sqlite';
import { createDb, expoSqliteDriver } from 'forge-orm';
const db = await createDb({ schema, driver: expoSqliteDriver(SQLite.openDatabaseSync('app.db')) });

// SQLite on the edge / Turso
import { createClient } from '@libsql/client';
import { createDb, libsqlDriver } from 'forge-orm';
const db = await createDb({ schema, driver: libsqlDriver(createClient({ url: process.env.TURSO_URL! })) });

// SQLite in the browser — sqlite-wasm + OPFS in a Web Worker.
// Full chapter at "Browser (sqlite-wasm + OPFS)" below.
import { createDb, wasmSqliteDriver } from 'forge-orm';
const worker = new Worker(new URL('forge-orm/wasm/worker', import.meta.url), { type: 'module' });
const db = await createDb({ schema, driver: wasmSqliteDriver({ worker, url: 'opfs-sahpool:///app.sqlite' }) });
await db.$migrate();   // runtime DDL apply (browser replacement for `forge push`)

// Postgres via postgres.js
import postgres from 'postgres';
import { createDb, postgresJsDriver } from 'forge-orm';
const db = await createDb({ schema, driver: postgresJsDriver(postgres(process.env.DATABASE_URL!)) });

// MySQL via the MariaDB connector (pass bigIntAsNumber/insertIdAsNumber for mysql2 parity)
import mariadb from 'mariadb';
import { createDb, mariadbDriver } from 'forge-orm';
const pool = mariadb.createPool({ host, user, database, bigIntAsNumber: true, insertIdAsNumber: true });
const db = await createDb({ schema, driver: mariadbDriver(pool) });

// MongoDB with your own client (custom TLS/auth/pool options, a shared client,
// or a Mongo-API backend: Amazon DocumentDB, Azure Cosmos DB, FerretDB)
import { MongoClient } from 'mongodb';
import { createDb, mongoDriver } from 'forge-orm';
const db = await createDb({ schema, driver: mongoDriver(new MongoClient(uri, { tls: true }), 'mydb') });

// DuckDB (embedded analytics — auto-loads the `spatial` extension at connect)
import { DuckDBInstance } from '@duckdb/node-api';
import { createDb, duckdbDriver } from 'forge-orm';
const instance = await DuckDBInstance.create('analytics.duckdb');
const connection = await instance.connect();
const db = await createDb({ schema, driver: duckdbDriver(connection) });

// SQL Server (Linux / Windows; ARM Macs auto-swap to azure-sql-edge in tests)
import sql from 'mssql';
import { createDb, mssqlDriver } from 'forge-orm';
const pool = await sql.connect({ server: 'localhost', user: 'sa', password: '', database: 'app' });
const db = await createDb({ schema, driver: mssqlDriver(pool) });

Each port is a small interface, so any other client fits too:

  • SQLite (SqliteDriver) — all, get, run, exec, close, optional iterate.
  • Postgres (PostgresDriver) / MySQL (MysqlDriver) — query + transaction + close, optional stream.
  • MongoDB (MongoDriver) — a pre-built MongoClient (plus an optional database name).
  • DuckDB (DuckdbDriver) — run / all over the @duckdb/node-api connection.
  • MSSQL (MssqlDriver) — query + transaction over a mssql pool.

One caveat: forge push / applyMigration (DDL) still assume each database's default driver. With an injected driver, run runtime queries through forge and manage schema/DDL with the default client (or separately).

Wire-compatible databases (no new code needed)

Several databases speak the wire protocol of one of the six forge supports. They work today through the matching adapter — point the existing driver at them:

Database Adapter How
CockroachDB postgres pg or postgresJsDriver against the CockroachDB URL
YugabyteDB postgres pg or postgresJsDriver
Neon postgres pg or @neondatabase/serverless wrapped in a PostgresDriver port
Supabase postgres pg against the Supabase URL
TimescaleDB postgres pg (TimescaleDB is a Postgres extension)
TiDB mysql mysql2Driver
PlanetScale mysql planetscaleDriver — built in
AWS DocumentDB mongo mongoDriver(new MongoClient(documentDbUri), dbName)
Azure Cosmos DB (Mongo API) mongo mongoDriver(new MongoClient(cosmosUri), dbName)
FerretDB mongo mongoDriver(new MongoClient(ferretUri), dbName)
Turso sqlite libsqlDriver — built in
Cloudflare D1 sqlite Wrap the D1 client in a thin SqliteDriver port (all/get/run/exec)
MotherDuck duckdb duckdbDriver against the MotherDuck token URL
Azure SQL Database mssql mssqlDriver against the Azure SQL URL
Azure SQL Edge mssql mssqlDriver — used as the ARM-Mac test fallback for SQL Server 2022

If your database isn't on the list and doesn't speak one of the six wire protocols, the answer is "implement the matching port interface" — same ~5-method surface every built-in driver implements.

Coming soon
Item Status Target
3D distance mode f.geoPoint({ dims: 3 }) round-trips altitude end-to-end; near / nearTo still compute ground (2D-on-sphere) distance. A 3D Euclidean or ground+vertical distance mode is the open question. TBD
Auto SRID reprojection Declared SRID is honoured at DDL time; the user provides coordinates in the target SRID's units. A built-in proj4-backed transform at the IR boundary is on the roadmap (avoids the per-app coordinate-transform boilerplate). TBD
Pre-built @forge-orm/sqlite-wasm-pro The custom wasm bundle (R-Tree + sqlite-vec) is one Emscripten command via scripts/wasm-pro/build.sh today; publishing the pre-built artifact as its own npm package is the next gap. TBD

If you need another database, file an issue. The bar to add a new adapter is ~10 small files: dialect, driver, ddl, compile-from-ir, execute, introspect, migrate, adapter, plus a few registration touches.


Defining a schema

A schema is a plain object mapping a name to a model. You build models with the helpers exported from forge-orm: f (fields), model, rel (relations), enums, and embed.

import { f, model, rel } from 'forge-orm';

const User = model('users', {
  id:         f.id(),
  email:      f.string().unique(),
  name:       f.string(),
  active:     f.bool().default(true),
  created_at: f.dateTime().default('now'),
  updated_at: f.dateTime().default('now').updatedAt(),
}).relate(() => ({
  posts: rel.many('post', { on: 'author_id', refs: 'id' }),
}));

const Post = model('posts', {
  id:        f.id(),
  author_id: f.objectId(),
  title:     f.string(),
  body:      f.text(),
}).relate(() => ({
  author: rel.one('user', { on: 'author_id', refs: 'id', onDelete: 'Cascade' }),
}));

export const schema = { user: User, post: Post } as const;

as const on the schema object is defensive, not required. For the pattern shown above — each model bound to its own const, then referenced from the schema literal — TypeScript already preserves the model types and the literal keys, so db.user.findFirst({ where: { … } }) autocompletes either way.

Models and automatic values (id, timestamps)

model(tableName, fields) declares a table (or a Mongo collection). The first argument is the real table name in the database; the object key you give it in the schema (user, post) is what you type as db.user.

forge fills in three kinds of value for you so you don't have to:

Primary key (f.id()). Every model has one. When you create a row without passing an id, forge generates one automatically on every database:

await db.user.create({ data: { email: 'a@x.co', name: 'A' } });  // id is generated

The default id is a string: an ObjectId on Mongo, and a UUID on Postgres, MySQL, SQLite, DuckDB, and MSSQL. It's a string (not a sequential number) so the same model is portable across all six databases. You can still pass your own id if you want to control it, and you can let the database generate it instead with a UUID default:

id: f.uuid({ default: 'gen_random_uuid' })   // Postgres/MySQL fill it in server-side

Created-at (f.dateTime().default('now')). Set to the current time when the row is created. You never pass it.

Updated-at (f.dateTime().default('now').updatedAt()). Set when the row is created and automatically bumped to the current time on every update, on all six databases. You never pass it.

const post = await db.post.create({ data: { title: 'Hi' } });
// post.created_at and post.updated_at are both set

await db.post.update({ where: { id: post.id }, data: { title: 'Hello' } });
// updated_at is now refreshed automatically

f.objectId() is for a column that holds another row's id (a foreign key). On Mongo it stores an ObjectId; on SQL it is plain text.

Picking a primary-key strategy

If you want something other than the default, pass f.id({ type }):

id: f.id()                            // default — app-generated string id (string in TS)
id: f.id({ type: 'auto' })            // same as the default; explicit form
id: f.id({ type: 'uuid' })            // DB-typed UUID column (PG `uuid`, MySQL `CHAR(36)`)
id: f.id({ type: 'bigserial' })       // auto-incrementing integer PK — number in TS

What each one emits per dialect:

Strategy Postgres MySQL SQLite DuckDB / MSSQL Mongo JS type
auto (default) text VARCHAR(64) TEXT TEXT / NVARCHAR(64) ObjectId string
uuid uuid CHAR(36) TEXT (same as auto) (same as auto) string
bigserial BIGSERIAL BIGINT NOT NULL AUTO_INCREMENT INTEGER PRIMARY KEY AUTOINCREMENT BIGINT … IDENTITY (MSSQL) throws at push number

bigserial is the SQL-only opt-in. Forge runs forge push on Mongo with a clear error if you use it ('bigserial' has no Mongo equivalent), so a schema mistake fails fast instead of half-applying. Use it when you're running a SQL-only service and you want classic integer keys; stay on auto or uuid for cross-DB portability.

With bigserial, the DB assigns the id — you don't pass one at create time, and Row<typeof Model>['id'] is typed as number:

const Order = model('orders', {
  id:     f.id({ type: 'bigserial' }),
  total:  f.int(),
});

const o = await db.order.create({ data: { total: 5_000 } });
o.id;          // ✓ number — TypeScript knows
await db.order.findFirst({ where: { id: 47 } });

Adding bigserial to an existing table? forge diff shows you the column change before you push, and forge diff apply writes a timestamped reconciliation migration if you'd rather review the SQL first.

Field types

Every field builder. Chain modifiers (.optional(), .unique(), .default(…), .searchable(), .softDeleteAt(), .dbgenerated(…), .updatedAt()) onto any of them.

Builder TS type Storage per dialect / notes
f.id() string Primary key, auto-generated when omitted. Pass { type: 'auto' | 'uuid' | 'bigserial' }.
f.objectId() string Foreign-key style. Mongo ObjectId; SQL TEXT.
f.string() string Short text. MySQL VARCHAR(255) (indexable). PG/SQLite/DuckDB TEXT, MSSQL NVARCHAR(255).
f.text() string Long text. MySQL TEXT. PG text, MSSQL NVARCHAR(MAX), DuckDB TEXT.
f.int() number 32-bit integer.
f.float() number Floating point.
f.decimal({ precision, scale }) string Exact numerics (money). PG numeric(p,s) / MySQL DECIMAL(p,s) / SQLite NUMERIC / Mongo Decimal128. Returned as a string to avoid float-precision loss.
f.bigint() bigint 64-bit integer. Use 1n literals. PG bigint / MySQL BIGINT / SQLite INTEGER / Mongo Long.
f.uuid({ default? }) string UUID. Pass { default: 'gen_random_uuid' } for a server-side default on PG / MySQL.
f.bool() boolean Stored as 0/1 on MySQL and SQLite, decoded back to a boolean.
f.dateTime() Date Timestamp. Accepts a Date or an ISO string on input.
f.json() any Arbitrary JSON. jsonb on Postgres, JSON on MySQL / MSSQL, TEXT on SQLite.
f.enumOf(['A','B'] as const) 'A' | 'B' A fixed set of string values, checked by the database where supported.
f.embed(Shape) Shape One nested object. Stored as JSON on SQL, sub-document on Mongo.
f.embedMany(Shape) Shape[] A list of nested objects. Defaults to [].
f.stringArray() / f.intArray() string[] / number[] A list of scalars. Native array on Postgres, JSON elsewhere.
f.geoPoint({ srid?, fallback? }) { lng: number; lat: number } 2D geographic point (WGS84 / SRID 4326). PG geography(Point, 4326) (PostGIS) / MySQL POINT NOT NULL SRID 4326 / SpatiaLite geometry / DuckDB GEOMETRY (spatial) / MSSQL GEOGRAPHY / Mongo GeoJSON. Pair with method: 'spatial'. fallback: true stores JSON + Haversine post-filter when no extension is installed.
f.vector(dims, { metric? }) number[] Dense numeric vector for embeddings / semantic search. PG vector(N) (pgvector) / MySQL VECTOR(N) (9.0+) / SQLite JSON+sqlite-vec / DuckDB FLOAT[N] (vss HNSW) / MSSQL VECTOR(N) (2025+) / Mongo plain array + Atlas Vector Search. metric is 'cosine' (default), 'l2', or 'dot'. Pair with method: 'vector'.
Field modifiers

Chain these onto any field builder. Modifiers are immutable — they return a new Field with the modifier applied.

Modifier What it does Notes / dialect quirks
.optional() Allows null. The TS type becomes T | null. Maps to NULL in SQL DDL, no required-presence check on Mongo.
.unique() Adds a unique index on the column. Sparse-on-optional is automatic on Mongo.
.default(value) Static default applied at create when no value is passed. The value is literal — strings, numbers, booleans, objects, arrays.
.default('now') Current timestamp at create. Use on f.dateTime(). Drives the created_at pattern.
.default('autoId') Server-generated id (used internally by f.id()). Rarely needed by hand.
.updatedAt() Auto-bumped to the current time on every update. Combine with .default('now') for the canonical updated_at.
.searchable() Tells forge push to build the right full-text index for this column (see Full-text search). Postgres GIN tsvector, MySQL FULLTEXT, SQLite FTS5 shadow table + triggers, Mongo text, DuckDB fts. MSSQL: out-of-band (manual FULLTEXT CATALOG).
.softDeleteAt() Marks this f.dateTime() column as the soft-delete column (see Soft delete). Forces optional. Reads auto-filter WHERE col IS NULL. One per model.
.dbgenerated('expr') Database-computed column. The wrapper never writes it; the DB evaluates <expr> on every change. PG / MySQL emit GENERATED ALWAYS AS (<expr>) STORED; SQLite uses the same shape; Mongo warns and skips.
f.string().optional()                                       // value can be null
f.string().unique()                                         // unique index on this column
f.string().default('pending')                               // static default
f.bool().default(true)                                      // bool default
f.dateTime().default('now')                                 // created-at
f.dateTime().default('now').updatedAt()                     // updated-at (set on create AND auto-bumped on update)
f.text().searchable()                                       // build a full-text index
f.dateTime().softDeleteAt()                                 // mark as the soft-delete column
f.decimal({ precision: 12, scale: 2 })
  .dbgenerated('"price" * "qty"')                           // computed by the DB
Indexes and unique constraints

Pass an options object as the third argument to model. The same IndexDef shape carries every common production index family — partial, expression, covering, geospatial, vector, hashed, wildcard, full-text, and more. Each field that doesn't apply on a given dialect is dropped at push with a clear warning, so one schema can target Mongo and SQL.

const Post = model('posts', {
  id:        f.id(),
  author_id: f.objectId(),
  slug:      f.string().unique(),     // single-column unique
  status:    f.enumOf(['DRAFT', 'PUBLISHED'] as const),
}, {
  indexes: [{ keys: { author_id: 1, status: 1 } }],   // a two-column index
  uniques: [['author_id', 'slug']],                   // a combined unique
});

The full index vocabulary:

indexes: [
  // Partial index. Mongo uses partialFilterExpression; Postgres and SQLite
  // use `where` with a raw SQL string. The same entry can carry both so
  // the schema works on either side.
  { keys: { sku: 1 }, unique: true,
    where: 'deleted_at IS NULL',
    partialFilterExpression: { deleted_at: { $exists: false } } },

  // TTL (Mongo).
  { keys: { createdAt: 1 }, expireAfterSeconds: 60 * 60 * 24 },

  // Mongo geospatial — `$near` / `$geoWithin` queries need this.
  { keys: { location: '2dsphere' } },

  // Spatial index — portable across dialects. Forge resolves the right
  // native family per dialect (PostGIS GIST, MySQL SPATIAL, DuckDB RTREE,
  // MSSQL CREATE SPATIAL INDEX, Mongo 2dsphere, SQLite virtual rtree).
  // Pair with f.geoPoint().
  { keys: { location: 1 }, method: 'spatial' },

  // Vector index — pgvector HNSW / DuckDB vss HNSW / MSSQL VECTOR HNSW.
  // Mongo and SQLite log a clean warning (their vector index is created
  // out-of-band). Pair with f.vector(N, { metric }).
  { keys: { embedding: 1 }, method: 'vector' },

  // Mongo hashed — required for a hashed shard key.
  { keys: { tenant: 'hashed' } },

  // Mongo case-insensitive unique. strength: 2 = case-insensitive.
  { keys: { email: 1 }, unique: true,
    collation: { locale: 'en', strength: 2 } },

  // Mongo wildcard index — keys: { '$**': 1 } + wildcardProjection.
  { keys: { '$**': 1 } as any, wildcardProjection: { 'data.$**': 1 } },

  // Postgres GIN over a jsonb column — supports @> containment.
  { keys: { tags: 1 }, method: 'gin' },

  // Postgres covering — answers (customer_id) → (status, total) from the index.
  { keys: { customer_id: 1 }, include: ['status', 'total'] },

  // Postgres BRIN for huge append-only tables (logs, analytics).
  { keys: { received_at: 1 }, method: 'brin' },

  // Expression index. Postgres / MySQL 8+ / SQLite. Mongo skips with a warning.
  { keys: {}, expression: 'lower(email)' },

  // MySQL fulltext with parser plugin — `'ngram'` for CJK, `'mecab'` for Japanese.
  { keys: { body: 1 }, method: 'fulltext', parser: 'ngram' },

  // MySQL 8+ invisible index — the optimizer ignores it,
  // useful for canary-testing whether an index is load-bearing before drop.
  { keys: { obsolete: 1 }, visible: false },

  // MySQL 8+ multi-valued index on a JSON array column — index every element
  // of the array. Use `expression` with the CAST that MySQL requires.
  { keys: {}, expression: "(CAST(tags->'$[*]' AS UNSIGNED ARRAY))" },
]

What each field does, per dialect:

Field Mongo Postgres MySQL SQLite DuckDB MSSQL
keys: { col: 1 / -1 } yes yes yes yes yes yes
keys: { col: 'text' } text index text_pattern_ops column kept column kept column kept column kept
keys: { col: '2dsphere'/'2d'/'hashed' } yes ignored ignored ignored ignored ignored
unique / sparse yes yes (sparse auto on optional) yes/n/a yes/n/a yes/n/a yes/n/a
expireAfterSeconds yes n/a n/a n/a n/a n/a
partialFilterExpression yes n/a n/a n/a n/a n/a
where (object) alias of PFE translated to SQL warn + skip translated to SQL warn + skip translated to SQL
where (SQL string) n/a WHERE … warn + skip WHERE … warn + skip WHERE …
include: [cols] n/a INCLUDE (…) warn + skip warn + skip warn + skip INCLUDE (…)
expression: 'sql' warn + skip ((expr)) ((expr)) (expr) ((expr)) warn + skip
method: gin/gist/brin/hash n/a USING … warn (ignored) warn (ignored) warn (ignored) warn (ignored)
method: 'spatial' resolves to 2dsphere USING GIST SPATIAL INDEX virtual rtree USING RTREE CREATE SPATIAL INDEX
method: 'vector' warn (Atlas search) USING hnsw (... opclass) warn (community = exact) warn (use sqlite-vec) USING HNSW USING VECTOR WITH (algorithm='HNSW')
method: 'fulltext' n/a DB rejects statement prefix warn (ignored) n/a warn (ignored)
parser: 'ngram'/'mecab' n/a warn (ignored) WITH PARSER … (only on fulltext) warn (ignored) warn (ignored) warn (ignored)
visible: false n/a warn (ignored) INVISIBLE (MySQL 8) warn (ignored) warn (ignored) warn (ignored)
collation yes n/a (use expression) n/a n/a n/a n/a
wildcardProjection yes n/a n/a n/a n/a n/a
Relations

A relation says "this model points at that model." You declare it with .relate(), which takes a function returning a map of relation names. There are two kinds:

  • rel.one(target, { on, refs }) is the side that holds the foreign key. For example a post has one author, and the post row stores author_id.
  • rel.many(target, { on, refs }) is the other side, a list. A user has many posts. Nothing is stored on the user row; forge looks posts up by their author_id.

The options mean:

  • target is the key in your schema map of the model you are pointing at ('user', not the table name 'users').
  • on is the column that holds the foreign key value.
  • refs is the column it points to on the other model (usually 'id').
  • onDelete (one-side only) controls what happens to this row when the row it points to is deleted: 'Cascade' (delete this too), 'SetNull' (clear the foreign key), 'Restrict', or 'NoAction'.
const User = model('users', { id: f.id(), name: f.string() })
  .relate(() => ({
    posts: rel.many('post', { on: 'author_id', refs: 'id' }),
  }));

const Post = model('posts', { id: f.id(), author_id: f.objectId(), title: f.string() })
  .relate(() => ({
    author: rel.one('user', { on: 'author_id', refs: 'id', onDelete: 'Cascade' }),
  }));

A model can point at itself, which is how you build trees such as comment replies:

const Comment = model('comments', { id: f.id(), parent_id: f.objectId().optional() })
  .relate(() => ({
    parent:  rel.one('comment',  { on: 'parent_id', refs: 'id' }),
    replies: rel.many('comment', { on: 'parent_id', refs: 'id' }),
  }));

Once a relation exists you can load it with include (see Choosing fields) and write related rows in one call (see Writing related records).

Embedded objects

An embedded object is a fixed shape stored inside a row, as JSON on SQL databases and as a sub-document on Mongo. Use embed to declare the shape.

import { embed, f, model } from 'forge-orm';

const Address = () => embed('Address', {
  street: f.string(),
  city:   f.string(),
  zip:    f.string(),
});

const User = model('users', {
  id:      f.id(),
  name:    f.string(),
  address: f.embed(Address).optional(),       // one address
  history: f.embedMany(Address),              // a list, defaults to []
});

await db.user.create({ data: { name: 'A', address: { street: '1 Main', city: 'SF', zip: '94110' } } });

You can read into embedded fields with JSON path queries:

await db.user.findMany({
  where: { address: { path: 'city', eq: 'SF' } },
});

See more — docs/MODEL.md (full field catalogue + id strategies + views + generated columns), docs/EMBED.md (f.embed/f.embedMany/f.json + JSON-null markers + 5 worked patterns), docs/RELATIONS.md (relation shapes + cascade + deep includes + 6 worked patterns), docs/INDEXES.md (every IndexDef field + per-dialect emit + drift detection), docs/PRIMARY-KEYS.md (UUIDv7 / ULID / Snowflake / serial trade-offs), docs/FOREIGN-KEYS.md (onDelete / onUpdate / deferred), docs/ENUMS.md (per-dialect emit + evolution), docs/CHECKS.md (CHECK constraints + Mongo $jsonSchema), docs/GENERATED-COLUMNS.md (STORED vs VIRTUAL), docs/VIEWS.md and docs/MATERIALIZED-VIEWS.md, docs/TRIGGERS.md (DB-side procedural code).


Reading data

Every model has the read methods you expect.

await db.user.findMany({ where: { active: true }, take: 20 });
await db.user.findFirst({ where: { email: 'a@x.co' } });          // first match or null
await db.user.findUnique({ where: { id: 'u1' } });                 // by a unique field
await db.user.findFirstOrThrow({ where: { email: 'a@x.co' } });    // throws if missing
await db.user.findUniqueOrThrow({ where: { id: 'u1' } });          // throws if missing
await db.user.count({ where: { active: true } });
await db.user.aggregate({                                          // bucketed stats
  where: { active: true },
  _avg:  { age: true },
  _sum:  { credits: true },
});
Filtering with where

where accepts either a direct value or an operator object per field, plus AND, OR, and NOT.

await db.post.findMany({
  where: {
    status: 'PUBLISHED',                       // equals
    title:  { contains: 'forge' },             // text match
    views:  { gte: 100, lt: 1000 },            // ranges
    author_id: { in: ['u1', 'u2'] },           // any of
    OR: [
      { pinned: true },
      { created_at: { gt: new Date('2024-01-01') } },
    ],
  },
});
Operator reference

All operators, with the field kinds they apply to.

Operator Applies to Meaning
equals / = every field exact match (same as passing a value directly)
not every field inverse of equals (accepts a value or a nested filter)
in every field value is one of an array
notIn every field

Keywords