npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

forge-orm

v2.20.4

Published

Prisma-shape multi-database ORM/wrapper for MongoDB, PostgreSQL, MySQL, SQLite, DuckDB & SQL Server from one codebase — typed geo / vector / JSON path / full-text search, no codegen, no query engine. Bring your own schema.

Readme

forge-orm

One Prisma-shaped query API. Six databases. No codegen.

npm version downloads stars ci examples license types

Documentation · Quick start · Examples · Changelog


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

📖 The full documentation reads far better as a website: johnsonfash.github.io/forge-orm — same content, with a sidebar, search, and one page per topic instead of three thousand lines of scroll.

| | | | |---|---|---| | 🐘 PostgreSQL | 🐬 MySQL / MariaDB | 🪶 SQLite | | 🍃 MongoDB | 🦆 DuckDB | 🟦 SQL Server | | 🌐 Browser (sqlite-wasm) | 💾 Browser (IndexedDB) | ⚡ PGlite (embedded PG) |

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: '[email protected]', 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:, pglite:, 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:

  • Geo — f.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 similarity — f.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 queries — where: { 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 search — f.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 | | Node CLI (smallest) | Smallest possible forge-orm program | | Hono + PGlite REST API | Backend API, zero external DB | | Next.js + PGlite full-stack | App Router + Server Actions | | IndexedDB zero-install | Full CRUD + geo + vector + FTS, no wasm |

| Auto-runs on CodeSandbox | Why | |---|---| | DuckDB analytics | @duckdb/node-api is a native addon | | Bun + SQLite blog | Bun's SQLite is native |

| 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.$on("query", …) filtering mutating ops | | 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 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 | | Binary columns — f.bytes(), per-dialect storage, maxBytes enforced everywhere, why base64 is refused, the typed-array view window, driver unwrapping, drift detection | docs/BINARY.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.enumOf(...), 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, updateFirst/deleteFirst and the two-round-trip re-read they replace, atomic ops, nested writes, idempotency, optimistic+pessimistic concurrency, 8 worked patterns | docs/MUTATIONS.md | | Transactions — the ambient session (a repository joins without tx), thunk array form, 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 | | $explain — see a query without running it, both callback forms, the plan via analyze, why EXPLAIN ANALYZE is never emitted, per-dialect support | docs/EXPLAIN.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, snapshots and forge generate, ALTER COLUMN safety, renamedFrom, forge migrate status, drift rules, per-dialect emit, 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 | | vs drizzle-kit — an honest feature-by-feature comparison, what drizzle does better, and the staged plan to close each gap | docs/VS-DRIZZLE.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, include scoped at every depth, _count and relation-filter consistency, 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 | | IndexedDB (zero-install browser) — planner scoring, IDB versioning migrations, FTS via multiEntry, Haversine + brute-force fallback, quota + ITP, server-safety guard | docs/INDEXEDDB.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.20.4 — f.bytes() never worked on DuckDB. Writing binary threw Cannot create values of type ANY — the node bindings take neither a Uint8Array nor a Buffer, only their own blobValue() — and a read came back as a DuckDBBlobValue wrapper instead of bytes. Broken in both directions, and missed because that dialect's driver was not installed when the earlier bytes fix was written, so its copy was only ever read. The driver is a devDependency now and DuckDB runs in CI.

  • 2.20.3 — a one-column uniques found nothing on IndexedDB. { uniques: [['pendingId']] } compiled to a compound IDB index, whose keys are arrays — while the planner looked it up with a scalar. The row was written and readable by primary key, but every where on that column returned null/[] with no error. Writing it as f.string().unique(), or with two or more columns, was never affected. Index migration now compares key shape rather than just the index name, so a database that already has the broken index gets it rebuilt. Browser-only: Postgres, MySQL, SQLite and Mongo were all verified unaffected. The adapter had no execution coverage at all until now — regression-indexeddb.ts closes that.

  • 2.20.2 — an ObjectId in a where threw. where: { author_id: new ObjectId(id) } — the most ordinary query you can write against MongoDB, and the shape docs/MONGO.md promises works — failed with unknown operator 'buffer'. Deciding whether a value was a value or an operator container like { gte: 5 } used "object, not array, not Date", so every class instance was walked for operators, and Object.keys() on an ObjectId returns ['buffer']. Decimal128, Long, Binary, UUID and a raw Buffer all broke the same way. Only a plain object is parsed for operators now. Worth re-checking if you try/catch around forge queries: the throw is loud, but a catch turns it into a query that quietly does nothing. See CHANGELOG.md.

  • 2.20 — updateFirst / deleteFirst: one round trip instead of two. update and delete return the written row but throw when the filter matched nothing, so "update it and hand it back, or tell me it is not there" was written as updateMany plus a re-read of the same row — two queries on essentially every write path (measured in one consumer codebase: 330 updateMany call sites, 123 of them followed by that re-read). The new verbs are the same write returning null on a miss. On a model with a .softDeleteAt() column the old shape was three trips and still wrong, because a read is soft-delete filtered and cannot see a row the patch just soft-deleted; updateFirst is not filtered and returns the row from the write itself. See CHANGELOG.md.

  • 2.19 — four things forge only looked like it did. Each ran without complaint while doing something other than what it said. $transaction did not reach a repository layer — a callback that called a repository discarded the tx, so neither leg was in the transaction and the throw meant to undo them undid nothing; the session now lives in an AsyncLocalStorage and a repository joins without being threaded a handle. The array form gave no atomicity at all — it was Promise.all over writes that had already dispatched; it takes thunks now and refuses already-running promises, which is the one behaviour change in the release. The Mongo client was one per process, not one per createDb() — a second createDb({ url: B }) silently kept writing to A, and $disconnect() on either closed the connection under both. Soft-deleted rows came back through every include — the filter was applied at the top level only, so a "deleted" post still listed under its author; relation sub-selects are now scoped at every depth, along with groupBy, relation _count and relation filters, which had all disagreed with it. See CHANGELOG.md.

  • 2.18 — three things that silently returned or wrote wrong data. A keyset cursor ignored the sort direction, so orderBy: { createdAt: 'desc' } with a cursor asked for rows greater than the last row seen and page 2 re-served page 1. Mongo update never coerced its values, so an id string written through update was stored as a BSON String where create would have stored an ObjectId — and because where does coerce, those rows became invisible to every later query. Nested take / skip applied to the whole batch instead of per parent, so include: { posts: { take: 3 } } over ten users returned three posts in total. Also: divide is an exact division rather than a multiply-by-reciprocal, a Mongo relation filter throws instead of matching every row in the collection, and f.bytes() is a real binary field kind — see docs/BINARY.md.

  • 2.7 — malformed queries throw instead of silently doing something else. Unknown where operators ($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, upsert keeps the create seed when update increments 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 runs Args → 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 an explain string. Non-destructive migrations via native IDB versioning (add-field is a no-op, createIndex back-populates existing rows automatically, destructive changes go under pending). FTS via multiEntry token index (index-backed AND-of-tokens, not a full scan). Geo via Haversine JS + bbox prefilter (MultiPolygon with holes, _distanceMeters annotation). 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 cryptic ReferenceError. Ships at the forge-orm/indexeddb subpath export. See Browser (zero install) — IndexedDB.

  • 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 / string).


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 | | PGlite (embedded PG) | pglite: (e.g. pglite:./data) | npm install @electric-sql/pglite | | 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 | | Browser (IndexedDB) | idb: or indexeddb: | none — browser built-in |

Those are the same package names the per-dialect entry points (forge-orm/postgres, forge-orm/mysql, and so on) import statically, so the install step above does not change with how you choose to connect — see Connecting for what that choice actually decides.

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. (A per-dialect entry point trades that lazy load for a static import, which is the whole point of it — see option 2 below.)

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 always takes your schema plus something that tells forge how to reach the database. There are three ways to give it that, and it returns the same typed db handle — properties matching your model names — whichever you pick. Everything else in this README reads the same afterwards.

The choice between them is about bundling, not taste. A URL is resolved at runtime, a per-dialect entry point is resolved at build time, and a driver you construct yourself was never forge's to resolve.

| Situation | Use | |---|---| | Node server, script, tests | url | | Bundled Node — Lambda, Next.js/Vite SSR, esbuild | forge-orm/<dialect> | | Cloudflare Workers, Vercel Edge | driver, with an HTTP client (Neon, PlanetScale, libSQL) | | Client needs configuring (pool, SSL, extensions, HTTP driver) | driver | | A client forge has no factory for | driver |

Option 1 — url (the default)

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();

forge reads the prefix, works out which adapter that means, and loads it. This is the shortest call site, and it is the only one where a single environment variable moves you from SQLite in tests to Postgres in production with no code change at all.

The mechanism is worth knowing, because it is what limits this option. forge calls require(pkg), where pkg is a string computed at runtime from the URL prefix. A bundler cannot see through a computed require: webpack, rollup, esbuild and Vite all lose the dependency at that line. On a bundled target — Cloudflare Workers, Vercel Edge, a Lambda you bundle — the driver is either left out of the output or blows up at runtime, a long way from the code that caused it. The same opacity is why no adapter can be tree-shaken here; the bundler cannot prove you are not about to ask for the other five.

None of that bites when node_modules is still on disk at runtime. Ordinary Node servers, scripts and test runs are exactly that case, and url is the right answer for them.

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 });

Option 2 — a per-dialect entry point (new in 2.17.0)

Import createDb from the entry point for the database you are on, then call it exactly as in option 1:

import { createDb, f, model } from 'forge-orm/postgres';

const User = model('users', { id: f.id(), email: f.string().unique() });
const db = await createDb({ url: process.env.DATABASE_URL!, schema: { user: User } });

Inside that entry, pg is brought in with a static import. A bundler sees it the way it sees any other import, keeps it, and drops the adapters you did not reach for. The call site is no longer than the default.

This is for bundled Node, not for true edge runtimes. A bundled Lambda, a Next.js or Vite server build, a Docker image built with esbuild — those run pg and better-sqlite3 perfectly well and only ever had a bundler problem. Cloudflare Workers and Vercel Edge are a different thing: pg needs a TCP socket and better-sqlite3 is a native addon, so neither runs there at all and no amount of bundler visibility changes that. On those runtimes you need a client built for them — Neon or PlanetScale over HTTP, libSQL, postgres.js — and you reach those through option 3, which was never bundler-hostile in the first place, because there you write the import yourself.

What you give up is the environment-variable swap. forge-orm/postgres is a Postgres call site; pointing DATABASE_URL at MySQL changes the connection string but not the driver the module has already committed to. Swapping database now means editing the import.

Available entries: forge-orm/postgres, forge-orm/mysql, forge-orm/sqlite, forge-orm/pglite, forge-orm/mongo, forge-orm/duckdb, forge-orm/mssql — one per package, each wrapping that dialect's default driver (pg, mysql2, better-sqlite3, PGlite, the mongodb client, @duckdb/node-api, mssql).

Per package, not per dialect, and pglite is why. PGlite is Postgres — forge runs it on the postgres adapter, same compiler, same executors, same dialect; only the driver differs, because one talks to a server over TCP and the other is a WebAssembly module in your own process. They are the same database and two different npm packages, and a static import can only pin one. A single forge-orm/postgres that imported both would put a WASM Postgres into every bundle that only wanted pg, which is the opposite of the point.

SQLite makes the same point louder: one dialect, six packages, all of them the sqlite adapter with identical queries and identical SQL. No two can share an entry point, because each would drag in a package the others cannot even load — forge-orm/sqlite is the better-sqlite3 one, which is why it belongs on a server and nowhere else.

Fifteen driver packages sit behind the six dialects:

| Dialect | Packages | Default (the entry point) | The rest — option 3 | |---|---|---|---| | SQLite | 6 | better-sqlite3 | expo-sqlite, OP-SQLite, @libsql/client, @sqlite.org/sqlite-wasm, @tauri-apps/plugin-sql | | Postgres | 3 | pg | postgres.js, @electric-sql/pglite (its own entry — see below) | | MySQL | 3 | mysql2 | mariadb, @planetscale/database | | MongoDB | 1 | mongodb | your own MongoClient — DocumentDB, Cosmos, FerretDB | | DuckDB | 1 | @duckdb/node-api | — | | SQL Server | 1 | mssql | — | | IndexedDB | 0 | none — forge-orm/indexeddb, built into the browser | — |

Nothing in the last column is second-class for being there. driver: is how a React Native app, a Turso deployment or a browser tab was always going to connect, and it is the only form that lets you configure the client.

The same logic covers the pg-compatible clients that are not here — postgres.js, Neon, Supabase's pooler. They all speak Postgres and forge treats them as Postgres, but each is its own package, and you are importing it yourself anyway, so they go through option 3:

import postgres from 'postgres';
import { createDb, postgresJsDriver } from 'forge-orm';

const db = await createDb({ schema, driver: postgresJsDriver(postgres(url)) });

There is deliberately none for the alternative drivers in the table under option 3 — postgres.js, MariaDB, PlanetScale, libSQL, Expo, OP-SQLite, Tauri. That is not an omission. An entry point exists to make a driver visible to a bundler when forge is the one choosing the package; the moment you pick a non-default client you are importing it yourself, and a static import is what you already have. forge-orm/libsql would save you one line and add a module to keep in step with somebody else's releases.

Each dialect entry re-exports everything the main entry exports — f, model, rel, the type helpers, the driver factories — so a single import line covers a whole file and you never end up pulling forge in from two places.

One caveat: a dialect entry still takes a url, so it configures the connection and nothing else. If the client itself needs options, that is option 3.

Do not reach for a dialect entry off the server. forge-orm/sqlite pulls in better-sqlite3, a native Node addon: import it from a React Native app and Metro fails on a module that cannot exist there, and in a browser bundle it is the same story. Those targets are not an afterthought — they have entry points and drivers of their own, and the full map is:

| Target | Import | Driver | |---|---|---| | Node, driver chosen at runtime | forge-orm | resolved from the URL prefix | | Bundled Node (Lambda, SSR, esbuild) | forge-orm/postgres /mysql /sqlite /pglite /mongo | that dialect's default, statically imported | | Browser — SQLite over OPFS | forge-orm/wasm (+ /wasm/worker, /wasm/vite, /wasm/next, /wasm/webpack) | wasmSqliteDriver | | Browser — zero install | forge-orm/indexeddb | built in, no package | | React Native (Expo) | forge-orm | driver: expoSqliteDriver(…) | | React Native (bare) | forge-orm | driver: opSqliteDriver(…) | | Tauri 2 desktop + mobile | forge-orm | driver: tauriSqlDriver(…) | | Cloudflare Workers, Vercel Edge | forge-orm | driver: with an HTTP client — Neon, PlanetScale, libSQL | | DuckDB, SQL Server | forge-orm/duckdb, forge-orm/mssql | @duckdb/node-api, mssql, statically imported |

Everything in the right-hand column of the last five rows goes through option 3, and none of them wants an entry point: you are importing the client yourself already, so the import is static and the bundler — Metro included — can see it. The entry points exist for the one case where forge, not you, picks the package.

Option 3 — driver (bring your own client)

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

This is required, not merely preferred, whenever the client needs configuring, because a connection string has nowhere to put that: pool size, SSL or TLS settings, statement and connection timeouts, the HTTP drivers Neon and PlanetScale use over their serverless endpoints, PGlite extensions, or a MongoClient you already share with the rest of the app. It is also how you use a client forge ships no factory for — you write the construction, so forge never needs to know the package exists. And because that import is yours and static, this option bundles as cleanly as option 2 does.

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), tauriSqlDriver (@tauri-apps/plugin-sql — Tauri 2 desktop + mobile) | | 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' }) });

// SQLite in a Tauri 2 app — @tauri-apps/plugin-sql (sqlx on Rust side).
import Database from '@tauri-apps/plugin-sql';
import { createDb, tauriSqlDriver } from 'forge-orm';
const sqlite = await Database.load('sqlite:app.db');
const db = await createDb({ schema, driver: tauriSqlDriver(sqlite) });
await db.$migrate();  // runtime DDL on first boot
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).

Browser (zero install) — IndexedDB

The IndexedDB adapter is forge's zero-install browser tier. Every browser has IndexedDB natively, so there's no wasm to download, no worker file to bundle, no COOP / COEP headers to set. Trade native SQL power for zero install:

import { createDb } from 'forge-orm';

const db = await createDb({ url: 'idb:appname', schema });
await db.user.create({ data: { email: '[email protected]', name: 'Alice' } });

The full Prisma-shape API works — reads, writes, relations, sorts, paging, aggregations, JSON path queries, geo near / withinPolygon, vector near / nearTo, full-text search, upsert, soft-delete + restore, .compile, $transaction, $migrate, $doctor, $diff.

The tradeoff vs sqlite-wasm:

| | sqlite-wasm | IndexedDB | |---|---|---| | Bundle cost | ~1 MB wasm + worker | zero | | Query engine | real SQL | IR → cursor scan + JS predicate | | Vector | native (sqlite-vec HNSW) | brute-force JS (< 1 k rows) | | Geo | native (R-Tree via SpatiaLite) | Haversine JS + bbox prefilter | | FTS | FTS5 with BM25 | multiEntry token index, AND-of-tokens | | Multi-tab | needs SAHPool VFS | native |

Both adapters read the same schema and take the same query calls, so swapping between them is a URL change.

Deep dive: docs/INDEXEDDB.md.

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: '[email protected]', 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 da