An Entity Framework-inspired ORM for TypeScript.
Model ordinary classes, write typed queries, track changes, and evolve schemas across SQLite, Postgres, and MySQL.
Install · Quick start · Docs · Model · Packages · Migrations · Learn · Alpha
Important
EntityKit is prerelease software; APIs may change before 1.0. This branch is
preparing the coordinated 0.1.0-alpha.3 family. The published alpha.2
includes @entitykit/nestjs and the source-backed DbContext constructor.
Applications upgrading from alpha.1 must select a shared source explicitly
with options.useDataSource(source) in configure() until they upgrade.
With a configured context, application code looks like this:
const user = db.users.create({ id: "usr_1", email: "[email protected]", name: "Ada", }); await db.saveChanges(); const users = await db.users .where(user => user.email.endsWith("@example.com")) .orderBy(user => user.name) .toArray();
The quick start below includes the complete model and context setup.
Install the provider-neutral runtime, one database provider, and the CLI:
npm install @entitykit/core@alpha @entitykit/sqlite@alpha npm install -D @entitykit/cli@alpha
| Database | Provider | Driver |
|---|---|---|
| SQLite | @entitykit/sqlite |
Built into Node |
| Postgres | @entitykit/postgres |
pg |
| MySQL | @entitykit/mysql |
mysql2 |
# Postgres npm install @entitykit/core@alpha @entitykit/postgres@alpha pg # MySQL npm install @entitykit/core@alpha @entitykit/mysql@alpha mysql2
EntityKit requires Node 22.13 or newer. Importing @entitykit/core does not
load a provider or database driver.
Enable typed predicate linting
in your editor and CI before writing queries. It catches JavaScript && and
|| on EntityKit predicate objects.
Define an ordinary class and map it in a DbContext:
import { DbContext, type ModelBuilder, } from "@entitykit/core"; import { createSqliteDataSource } from "@entitykit/sqlite"; type NewUser = { id: string; email: string; name: string }; class User { id: string; email: string; name: string; constructor(input: NewUser) { this.id = input.id; this.email = input.email; this.name = input.name; } } class AppDbContext extends DbContext { readonly users = this.set(User); protected override model(model: ModelBuilder): void { model.entity(User, entity => { entity.toTable("users"); entity.hasKey(user => user.id); entity.property(user => user.id).hasColumnType("text").isRequired(); entity.property(user => user.email).hasColumnType("text").isRequired(); entity.property(user => user.name).hasColumnType("text").isRequired(); entity.materializeChecked(row => new User({ id: row.required(user => user.id), email: row.required(user => user.email), name: row.required(user => user.name), })); entity.hasIndex(user => user.email).isUnique(); }); } } const dataSource = createSqliteDataSource("./app.db");
materializeChecked() lets the callback construct a fresh entity and check
the scalar values it requests. Errors identify the entity and property.
EntityKit then assigns the captured mapped values, including configured read conversions. Constructor-only transformations can be overwritten; unrequested values receive no additional checks. See materialization for nullable fields, read conversions, and custom domain values.
Create a local schema, write a row, query it, and save a tracked change.
users.create() constructs and tracks the entity; it executes no SQL.
saveChanges() persists the pending work:
try { await using db = dataSource.createContext(AppDbContext); await db.database.ensureCreated(); const user = db.users.create({ id: "usr_1", email: "[email protected]", name: "Ada", }); await db.saveChanges(); const loaded = await db.users .where(candidate => candidate.email.eq("[email protected]")) .single(); loaded.name = "Ada Lovelace"; await db.saveChanges(); } finally { await dataSource.dispose(); }
The lifecycle is deliberate: create one provider data source for the
application, create a fresh context for every request, job, or other unit of
work, dispose that context, then dispose the data source during application
shutdown. DbContext accepts the source in its optional constructor, so a
source-backed context needs no provider-specific configure() method.
ensureCreated() is convenient for a one-time prototype or disposable-database
bootstrap; it is not a deployment or schema-evolution primitive. Once a schema
must evolve without losing data, use source-controlled migrations.
|
Domain model Plain TypeScript classes, explicit fluent mapping, value converters, complex properties, keys, indexes, and relationships. |
Typed queries Filters, ordering, paging, projections, joins, aggregates, relationship predicates, streaming, and parameterized SQL. |
|
Unit of work Identity-map materialization, snapshot change tracking, transactional saveChanges(), savepoints, and optimistic concurrency.
|
Explicit relationships Split-query includes, filtered collections, explicit loading, and opt-in awaitable lazy loading—never hidden property-access I/O. |
|
Schema workflow Model snapshots, generated migrations, dry runs, SQL scripts, guarded destructive changes, and database-first model generation. |
Application controls Tenant scopes, soft deletes, audit fields, outbox rows, cancellation, diagnostics, retries, and provider-neutral test doubles. |
EntityKit does not require decorators, generated clients, function-source parsing, or hidden lazy loading. Query selectors build typed expression trees; values stay separate from generated SQL.
| Package | Role |
|---|---|
@entitykit/core |
Contexts, mapping, queries, tracking, errors, and configuration |
@entitykit/sqlite |
SQLite provider using Node's built-in node:sqlite |
@entitykit/postgres |
Postgres provider using pg |
@entitykit/mysql |
MySQL provider using mysql2 |
@entitykit/cli |
Migrations, database inspection, and scaffolding |
@entitykit/testing |
Provider-neutral recording test doubles |
@entitykit/nestjs |
Native-ESM NestJS 12 lifecycle integration; begins in alpha.2 |
Core also exposes focused /migrations, /tooling, and /adapter entry
points. /experimental contains unstable compiler and builder internals.
The CLI creates configuration and context files, compares the current model with its checked-in snapshot, and produces reviewable TypeScript migrations:
npx entitykit init
# add entities and mappings to src/db/app-db-context.ts
npx entitykit migration add InitialCreate
npx entitykit db migrate --dry-run
npx entitykit db migrateRename hints preserve data when a model name changes. Destructive forward
operations are reported and require explicit --allow-data-loss approval.
Generated migrations are source code: review them before applying them.
The migration guide covers renames, rollback planning, deployment scripts, database-first projects, and provider-specific DDL rules.
- Documentation map — the shortest path for newcomers, framework users, contributors, and release maintainers.
- Usage guide — build a real context, query, save, load relationships, run transactions, and manage migrations.
- API reference — packages, entry points, public operations, errors, and extension surfaces.
- Framework guide — NestJS 12 and the Node-runtime Next.js 16 integration pattern.
- Next.js + Postgres demo — a production-shaped
App Router example pinned to the exact
alpha.3workspace family. - Compatibility — Node support, provider parity, and current alpha boundaries.
- Architecture — package ownership, provider seams, and the query/save pipelines.
EntityKit is deliberately honest about its current boundary. The
0.1.0-alpha.1 release was the original six-package family; the coordinated
0.1.0-alpha.2 release added the NestJS package and the refined data-source
lifecycle API. This branch prepares 0.1.0-alpha.3, with aggregate correctness,
nested-savepoint cleanup, performance repairs and both published upgrade baselines.
- Every package in a coordinated release moves on one exact prerelease version.
- The public API may change before 1.0;
/experimentalhas no compatibility promise during alpha. - Provider-neutral behavior is shared, but database DDL, isolation, locking, collation, and schema-introspection details still differ.
- Migration generation is conservative. Renames and destructive changes need explicit review.
- SQLite table rebuilds and MySQL's implicitly committed DDL require extra care.
The complete, provider-by-provider contract lives in Compatibility.
npm ci npm run verify
Read Contributing before changing package boundaries or a public API. Report suspected vulnerabilities through the private process in Security. Maintainers should follow the exact release runbook for the published alpha family.
MIT. See LICENSE.
compileModel(configure, { provider: 'sqlite' }) creates an owned mapping handle. Select it with options.useCompiledModel(handle) to skip the per-context model() hook while retaining fresh request state. The initial flat, callback-free subset and ownership limits are documented in compiled models. Eligible buffered entity reads reuse field plans, and flat projections resolve their fields once per execution. Constructors, value readers, verified writes and persistence ownership remain live.