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.7 — malformed queries throw instead of silently doing something else. Unknown
whereoperators ($gte,contians) used to be dropped from the tree, so the filter matched every row; typoed update operators ({ incrment: 5 }) were written through$set, replacing a number with an object. Both throw now, with the correction in the message ("Did you mean 'gte'? forge uses bare operator names").not: { contains: … }actually negates, strict mode recurses into AND/OR/NOT and relation filters,upsertkeeps thecreateseed whenupdateincrements the same field,aggregate([...])works positionally, dotted container paths ('address.city') compile portably on every dialect, and.optional()columns get their full operator set back in the types. - 2.6 — IndexedDB adapter: zero-install browser tier. A second browser adapter alongside sqlite-wasm — no wasm download, no worker file, no bundler plugin. URL prefix
idb:/indexeddb:selects the adapter and the string after the colon is the IDB database name. The Prisma-shape surface is identical to every other dialect; the executor pipeline runsArgs → IR → planner picks ONE index → cursor scan → JS residual filter → JS sort → limit/offset. Selectivity-scored index selection (primary-key eq > compound eq > unique eq > compound eq > single-column eq >in> range > free sort > full scan), every plan carries anexplainstring. Non-destructive migrations via native IDB versioning (add-field is a no-op,createIndexback-populates existing rows automatically, destructive changes go underpending). FTS via multiEntry token index (index-backed AND-of-tokens, not a full scan). Geo via Haversine JS + bbox prefilter (MultiPolygon with holes,_distanceMetersannotation). Vector via JS brute-force cosine / l2 / dot (fine ≤ 1 k rows). Cascade walker matches the Mongo pattern. Server-safety guard throws a specific[P2010]message on Node / SSR instead of a crypticReferenceError. Ships at theforge-orm/indexeddbsubpath export. See Browser (zero install) — IndexedDB. - 2.5 — MSSQL
MERGEupsert, Mongo cross-fieldnearTo, 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 properMERGE INTO … USING (VALUES) … WHEN MATCHED THEN UPDATE … WHEN NOT MATCHED THEN INSERT OUTPUT inserted.*(atomic, returns the row). Mongonearfilter on field A +nearToorderBy on field B now both fire — cross-field rewrite emits$geoWithin: { $centerSphere }for A inside the$geoNear.queryso the single-stage limit doesn't drop A.db.$doctor()anddb.$diff()are the browser equivalents of theforge doctor/forge diffCLIs — returns structured reports your app can render.withinPolygonaccepts 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 (PGgeography(PointZ), SQLitePOINT Z, DuckDBST_Point3D, MSSQLPOINT(x y z)); distance ops remain ground-distance (2D-on-sphere) — altitude round-trips.f.geoPoint({ srid: 3857 })(or any non-4326) routes PG togeometry(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-wasmrunning in a Web Worker, persisted on the Origin Private File System. New URL schemesopfs:,opfs-sahpool:, and:memory:; a newwasmSqliteDriver()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 forforge 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:andmssql:URL prefixes); typed geo (f.geoPoint()+near/nearTo/withinPolygonacross 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 samenear/nearTovocabulary, compiling to pgvector / DuckDB vss / MSSQLVECTOR_DISTANCE/ Mongo$vectorSearch);forge doctorlive capability probe;forge push --enable-extensions; a throwaway driver smoke harness (npm run smoke:drivers). - 2.2 —
IndexDefcovers the shapesforge pushcouldn't model. SQL partial indexes (where: 'deleted_at IS NULL'), expression indexes (expression: 'lower(email)'), Postgres access methods (gin/gist/brin/hash) plusINCLUDEcovering columns, MySQLFULLTEXTparser 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
IndexDefnow acceptspartialFilterExpression, soforge pushcan 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 explicitsoftDelete()/restore()verbs. See Soft delete. - 1.9 — pluggable MySQL + Mongo. MySQL adds
mariadbDriverandplanetscaleDriveralongside the defaultmysql2; Mongo lets you bring your ownMongoClient(mongoDriver) for DocumentDB / Cosmos / FerretDB / custom options. - 1.8 — pluggable Postgres drivers. Use
postgres.js(porsager) instead ofnode-postgres, or any client you wrap, viacreateDb({ 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'shavingaccepts 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 awhere({ currentUsage: { lt: col('globalLimit') } }), portable across every dialect. - 1.4 — primary-key strategies on
f.id()(auto/uuid/bigserial/string).