{"_id":"@bcap/drizzle-history","_rev":"4-2d19278db0d7df672811a07a4619a285","name":"@bcap/drizzle-history","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@bcap/drizzle-history","version":"0.1.0","keywords":["drizzle","drizzle-orm","history","audit","audit-log","versioning","temporal","postgres","postgresql","pglite","database","typescript"],"author":{"name":"Blockchain Capital"},"license":"BSD-2-Clause","_id":"@bcap/drizzle-history@0.1.0","maintainers":[{"name":"evan.bcap","email":"evan.contractor@blockchaincapital.com"}],"homepage":"https://github.com/BlockchainCap/drizzle-history#readme","bugs":{"url":"https://github.com/BlockchainCap/drizzle-history/issues"},"dist":{"shasum":"e98bdbc3eba523a775cfeb7b2aa97dc70f485400","tarball":"https://registry.npmjs.org/@bcap/drizzle-history/-/drizzle-history-0.1.0.tgz","fileCount":32,"integrity":"sha512-P4Ms8EVG412eICwL9DRvRpEK3G+k++aq/0K8HpGgLQFrT0eIE0h/IRddM1JK6XARifS0FVXWNQQ13vma+4R5zA==","signatures":[{"sig":"MEQCIB7LJcak8u0Y6WvpKD0ugNEcuB4QZjWPLp3Fb858JYTIAiAwFz5qx9BpowxJ1fNoAQPHlRDzEG8+DK2ypG0aIOvrcg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1183063},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./kit":{"import":{"types":"./dist/kit.d.ts","default":"./dist/kit.js"},"require":{"types":"./dist/kit.d.cts","default":"./dist/kit.cjs"}}},"gitHead":"51b32bb3f340c72a3ecc8028b2f047c70a814eac","scripts":{"lint":"eslint .","test":"vitest run","bench":"vitest bench --config vitest.config.bench.ts","build":"tsup","check":"bun run typecheck && bun run typecheck:examples && bun run lint && bun run format:check && bun run test && bun run test:types && bun run test:types:consumer","format":"prettier --write .","test:pg":"DRIZZLE_HISTORY_TEST_DRIVER=postgres-js DATABASE_URL=postgres://postgres:postgres@localhost:5433/postgres vitest run","test:v1":"vitest run --config vitest.config.v1.ts","examples":"bun examples/quickstart.ts && bun examples/attribution.ts && bun examples/point-in-time.ts","lint:fix":"eslint . --fix","typecheck":"tsc --noEmit","test:types":"tsc --project type-tests/tsconfig.json","format:check":"prettier --check .","lint:package":"bun run build && publint && attw --pack .","typecheck:examples":"tsc --project examples/tsconfig.json","test:types:consumer":"bun run build && tsc --project type-tests/consumer/tsconfig.v0.json && tsc --project type-tests/consumer/tsconfig.v1.json"},"_npmUser":{"name":"evan.bcap","email":"evan.contractor@blockchaincapital.com"},"repository":{"url":"git+https://github.com/BlockchainCap/drizzle-history.git","type":"git"},"_npmVersion":"11.9.0","description":"Automatic history tracking for Drizzle ORM tables on PostgreSQL","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"typesVersions":{"*":{"kit":["./dist/kit.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","eslint":"^9.20.0","vitest":"^3.2.0","publint":"^0.3.12","postgres":"^3.4.5","prettier":"^3.5.0","@eslint/js":"^9.20.0","fast-check":"^4.1.1","typescript":"^5.9.0","@types/node":"^22.10.0","drizzle-kit":"^0.31.10","drizzle-orm":"^0.45.0","drizzle-orm-v1":"npm:drizzle-orm@1.0.0-rc.4","typescript-eslint":"^8.20.0","@electric-sql/pglite":"^0.4.2","@arethetypeswrong/cli":"^0.18.0","eslint-config-prettier":"^10.0.0"},"peerDependencies":{"drizzle-kit":">=0.30.0 <0.32.0 || >=1.0.0-rc <2","drizzle-orm":">=0.45.0 <0.46.0 || >=1.0.0-rc <2"},"peerDependenciesMeta":{"drizzle-kit":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/drizzle-history_0.1.0_1783370835094_0.604059518595194","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bcap/drizzle-history","version":"0.1.1","keywords":["drizzle","drizzle-orm","history","audit","audit-log","versioning","temporal","postgres","postgresql","pglite","database","typescript"],"author":{"name":"Blockchain Capital"},"license":"BSD-2-Clause","_id":"@bcap/drizzle-history@0.1.1","maintainers":[{"name":"evan.bcap","email":"evan.contractor@blockchaincapital.com"}],"homepage":"https://github.com/BlockchainCap/drizzle-history#readme","bugs":{"url":"https://github.com/BlockchainCap/drizzle-history/issues"},"dist":{"shasum":"f5c6b19192fc45b5e087b4a1d1818cce79e7581a","tarball":"https://registry.npmjs.org/@bcap/drizzle-history/-/drizzle-history-0.1.1.tgz","fileCount":32,"integrity":"sha512-y30B578E8KMJX6weUEhtTPeT3LhkW4ch9tXdIwzMYLQ6OkHmxCC6fjfRje+nAl0ntMP31RxFj3IXisG2NHf/Dg==","signatures":[{"sig":"MEUCIEAGuxU+25I3azXDmpkhVk76e9P7Q+PTRHXhSRfS1Ly7AiEAps3bw5+5vXSjsSrqXtfUSNdfdQxIX89Bc+zr0j2cujM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1183059},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=22.0.0"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./kit":{"import":{"types":"./dist/kit.d.ts","default":"./dist/kit.js"},"require":{"types":"./dist/kit.d.cts","default":"./dist/kit.cjs"}}},"gitHead":"2074f63fe284faf8b4ddc696b6f156e0e96fc583","scripts":{"lint":"eslint .","test":"vitest run","bench":"vitest bench --config vitest.config.bench.ts","build":"tsup","check":"bun run typecheck && bun run typecheck:examples && bun run lint && bun run format:check && bun run test && bun run test:types && bun run test:types:consumer","format":"prettier --write .","test:pg":"DRIZZLE_HISTORY_TEST_DRIVER=postgres-js DATABASE_URL=postgres://postgres:postgres@localhost:5433/postgres vitest run","test:v1":"vitest run --config vitest.config.v1.ts","examples":"bun examples/quickstart.ts && bun examples/attribution.ts && bun examples/point-in-time.ts","lint:fix":"eslint . --fix","typecheck":"tsc --noEmit","test:types":"tsc --project type-tests/tsconfig.json","format:check":"prettier --check .","lint:package":"bun run build && publint && attw --pack .","typecheck:examples":"tsc --project examples/tsconfig.json","test:types:consumer":"bun run build && tsc --project type-tests/consumer/tsconfig.v0.json && tsc --project type-tests/consumer/tsconfig.v1.json"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:d716d747-b552-4142-a149-bc463f33d413"}},"repository":{"url":"git+https://github.com/BlockchainCap/drizzle-history.git","type":"git"},"_npmVersion":"11.16.0","description":"Automatic history tracking for Drizzle ORM tables on PostgreSQL","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"typesVersions":{"*":{"kit":["./dist/kit.d.ts"]}},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.0","eslint":"^9.20.0","vitest":"^3.2.0","publint":"^0.3.12","postgres":"^3.4.5","prettier":"^3.5.0","@eslint/js":"^9.20.0","fast-check":"^4.1.1","typescript":"^5.9.0","@types/node":"^22.10.0","drizzle-kit":"^0.31.10","drizzle-orm":"^0.45.0","drizzle-orm-v1":"npm:drizzle-orm@1.0.0-rc.4","typescript-eslint":"^8.20.0","@electric-sql/pglite":"^0.4.2","@arethetypeswrong/cli":"^0.18.0","eslint-config-prettier":"^10.0.0"},"peerDependencies":{"drizzle-kit":">=0.30.0 <0.32.0 || >=1.0.0-rc <2","drizzle-orm":">=0.45.0 <0.46.0 || >=1.0.0-rc <2"},"peerDependenciesMeta":{"drizzle-kit":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/drizzle-history_0.1.1_1783374024266_0.4874786120176806","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-07-06T20:47:14.918Z","modified":"2026-07-07T16:21:51.076Z","0.1.0":"2026-07-06T20:47:15.302Z","0.1.1":"2026-07-06T21:40:24.487Z"},"bugs":{"url":"https://github.com/BlockchainCap/drizzle-history/issues"},"author":{"name":"Blockchain Capital"},"license":"BSD-2-Clause","homepage":"https://github.com/BlockchainCap/drizzle-history#readme","keywords":["drizzle","drizzle-orm","history","audit","audit-log","versioning","temporal","postgres","postgresql","pglite","database","typescript"],"repository":{"url":"git+https://github.com/BlockchainCap/drizzle-history.git","type":"git"},"description":"Automatic history tracking for Drizzle ORM tables on PostgreSQL","maintainers":[{"email":"caleb@blockchaincapital.com","name":"starfishcap"},{"email":"zile@blockchaincapital.com","name":"zile_bcap"},{"email":"evan.contractor@blockchaincapital.com","name":"evan.bcap"}],"readme":"# @bcap/drizzle-history\n\nAutomatic history tracking for [Drizzle ORM](https://orm.drizzle.team) tables on PostgreSQL.\n\n@bcap/drizzle-history records every insert, update, and delete on a tracked table as a typed snapshot row in a companion history table, in the same transaction as the write itself.\nWrap a table once at declaration time and wrap the database once at construction time; everything else is ordinary drizzle code.\nThere are no database triggers, extensions, or background processes: history tables are plain `pgTable` definitions that drizzle-kit migrates and drizzle queries, and the capture mechanism is a proxy around the drizzle database object.\nThe design is inspired by [django-simple-history](https://github.com/jazzband/django-simple-history).\n\n## Features\n\n- One-call setup: `withHistory(table)` derives a typed companion history table, and `withHistory(db)` records every query-builder mutation against tracked tables.\n- Atomic by default: the source write and its history rows commit together or not at all.\n- Enforced attribution: `defineHistory({ metadataColumns })` requires `.setHistory({ ... })` on every write, checked at compile time and again at runtime.\n- Point-in-time reads with `asOf`, per-row timelines with `rowHistory`, snapshot diffs with `diffHistoryRows`, and restore building blocks with `sourceValuesFromHistory`.\n- Backfill existing rows with `populateHistory` and apply retention policies with `pruneHistory` (including a `dryRun` preview).\n- drizzle-kit integration: `defineConfigWithHistory` makes `generate`, `migrate`, `push`, and `studio` see the derived history tables.\n- Works with drizzle-orm 0.45.x and 1.x, PostgreSQL 13 and newer, and the postgres-js, node-pg, PGlite, and neon-http drivers.\n- Fully typed: history tables are real drizzle tables, so history rows come back as typed select models.\n\n## Installation\n\n```sh\nbun add @bcap/drizzle-history\n# or\nnpm install @bcap/drizzle-history\n# or\npnpm add @bcap/drizzle-history\n```\n\n`drizzle-orm` is a peer dependency with the supported range `>=0.45.0 <0.46.0 || >=1.0.0-rc <2`.\n`drizzle-kit` is an optional peer dependency, needed only for the `@bcap/drizzle-history/kit` entrypoint.\nNode.js 22 or newer is required, and both ESM and CJS consumers are supported.\n\n## Quickstart\n\nThe snippet below runs as-is against in-memory PostgreSQL via [PGlite](https://pglite.dev), so you can try the library without a database server.\nIt is also available as [examples/quickstart.ts](./examples/quickstart.ts).\n\n```ts\nimport { PGlite } from '@electric-sql/pglite';\nimport { withHistory } from '@bcap/drizzle-history';\nimport { eq, sql } from 'drizzle-orm';\nimport { pgTable, serial, text } from 'drizzle-orm/pg-core';\nimport { drizzle } from 'drizzle-orm/pglite';\n\n// Wrap the table at declaration time. This derives a companion `users_history`\n// table, reachable as `users.history.table`.\nconst users = withHistory(\n\tpgTable('users', {\n\t\tid: serial().primaryKey(),\n\t\tname: text().notNull(),\n\t\tstatus: text().notNull(),\n\t}),\n);\n\nconst client = new PGlite();\nconst db = drizzle({ client });\n\n// Wrap the database once. In an application, export only the wrapped instance.\nconst trackedDb = withHistory(db);\n\n// In a real project both tables come from drizzle-kit migrations (see\n// \"Adopting @bcap/drizzle-history on an existing project\"). This example inlines the DDL.\nawait db.execute(\n\tsql.raw(`\n\t\tcreate table users (\n\t\t\tid serial primary key,\n\t\t\tname text not null,\n\t\t\tstatus text not null\n\t\t)\n\t`),\n);\nawait db.execute(\n\tsql.raw(`\n\t\tcreate table users_history (\n\t\t\tid integer,\n\t\t\tname text,\n\t\t\tstatus text,\n\t\t\t\"historyId\" uuid primary key default gen_random_uuid(),\n\t\t\t\"historyType\" varchar(1) not null,\n\t\t\t\"historyDate\" timestamp with time zone not null default now()\n\t\t)\n\t`),\n);\n\n// Mutate through the wrapped database exactly like plain drizzle.\nawait trackedDb.insert(users).values({ name: 'Ada', status: 'pending' });\nawait trackedDb.update(users).set({ status: 'active' }).where(eq(users.id, 1));\nawait trackedDb.delete(users).where(eq(users.id, 1));\n\n// Each mutation captured one snapshot row in the history table, which is a\n// plain drizzle table with fully typed rows.\nconst history = users.history.table;\nconst snapshots = await db.select().from(history).orderBy(history.historyDate, history.historyId);\n\nconsole.log('type  id  name  status');\nfor (const row of snapshots) {\n\tconsole.log(`${row.historyType}     ${row.id}   ${row.name}   ${row.status}`);\n}\n// type  id  name  status\n// +     1   Ada   pending\n// ~     1   Ada   active\n// -     1   Ada   active\n```\n\nEvery history table carries three core columns plus a nullable mirror of each source column:\n\n| Column        | Type                       | Meaning                                                    |\n| ------------- | -------------------------- | ---------------------------------------------------------- |\n| `historyId`   | `uuid`, primary key        | Unique id of the snapshot, `gen_random_uuid()` by default. |\n| `historyType` | `varchar(1)`               | `+` for insert, `~` for update, `-` for delete.            |\n| `historyDate` | `timestamp with time zone` | When the snapshot was captured, `now()` by default.        |\n\nInserts and updates record the row as it looked after the write; deletes record the final values the row held.\nMirrored source columns are nullable and constraint-free by design, so history rows can outlive any source-side schema rules.\n\n## Attribution metadata\n\n`defineHistory({ metadataColumns })` creates a toolkit whose tracked tables all share extra metadata columns, such as who made a change and why.\nA metadata column is required exactly when it has no database default, and nullable columns without defaults are still required: the caller must pass `null` explicitly rather than omit the key.\n\n```ts\nimport { defineHistory } from '@bcap/drizzle-history';\nimport { integer, pgTable, serial, text } from 'drizzle-orm/pg-core';\n\nconst history = defineHistory({\n\tmetadataColumns: {\n\t\teditorId: integer('editor_id').notNull(),\n\t\treason: text('reason'),\n\t},\n});\n\nconst articles = history.withHistory(\n\tpgTable('articles', {\n\t\tid: serial().primaryKey(),\n\t\ttitle: text().notNull(),\n\t}),\n);\n\nconst trackedDb = history.withHistory(db);\n\nawait trackedDb\n\t.insert(articles)\n\t.values({ title: 'Hello, world' })\n\t.setHistory({ editorId: 1, reason: 'first draft' });\n\nawait trackedDb\n\t.update(articles)\n\t.set({ title: 'Hello, world!' })\n\t.where(eq(articles.id, 1))\n\t.setHistory({ editorId: 2, reason: null });\n```\n\nForgetting the metadata is a compile-time error, not a code-review convention.\nWhile any required key is missing, the builder type has no `execute`, `prepare`, or promise methods, and a bare `await` fails to compile:\n\n```ts\nawait trackedDb.insert(articles).values({ title: 'Hello, world' });\n// error TS1320: Type of 'await' operand must either be a valid promise or must\n// not contain a callable 'then' member.\n\ntrackedDb.insert(articles).values({ title: 'Hello, world' }).execute();\n// error TS2339: Property 'execute' does not exist on type 'HistoryMutationBuilder<...>'.\n```\n\nThe blocked builder's `then` signature carries the fix as a string literal type, so calling `.then(...)` directly spells it out:\n\n```ts\ntrackedDb\n\t.insert(articles)\n\t.values({ title: 'Hello, world' })\n\t.then(() => {});\n// error TS2345: Argument of type '() => void' is not assignable to parameter of type\n// '\"Call .setHistory(...) with every required history field before awaiting this builder.\"'.\n```\n\nMetadata can be supplied across multiple `.setHistory(...)` calls, and each call subtracts the keys it provides from the missing set.\nA runtime check enforces the same contract for untyped call sites: missing required keys and `null` values for `notNull` columns are rejected before any SQL runs.\nSee [examples/attribution.ts](./examples/attribution.ts) for a runnable version.\n\n## Querying history\n\nThe read-side helpers are standalone functions that accept any drizzle Postgres database or transaction, raw or wrapped, and issue only `SELECT`s.\n\n```ts\nimport { asOf, diffHistoryRows, rowHistory } from '@bcap/drizzle-history';\nimport { eq } from 'drizzle-orm';\n\n// Reconstruct the table as it existed at a point in time, with an optional\n// filter written against the source table's columns.\nconst activeLastWeek = await asOf(db, users, lastWeek, {\n\twhere: eq(users.status, 'active'),\n});\n\n// Every captured snapshot of one logical row, newest first by default.\nconst snapshots = await rowHistory(db, users, { id: 42 }, { limit: 10 });\n\n// Which source columns changed between two snapshots.\nconst { changed } = diffHistoryRows(users, snapshots[1]!, snapshots[0]!);\n// [{ column: 'status', before: 'pending', after: 'active' }]\n```\n\n`asOf` pushes the reconstruction down to PostgreSQL as a single query served by the history table's derived indexes, and drops rows whose latest snapshot is a delete.\n`rowHistory` accepts inclusive `since` and `until` bounds and returns fully typed history rows, including the core columns and any metadata columns.\n`diffHistoryRows` compares `Date`s by instant and JSONB values structurally, and takes optional `include` and `exclude` column lists.\nSee [examples/point-in-time.ts](./examples/point-in-time.ts) for a runnable timeline.\n\n## Restoring rows\n\n`sourceValuesFromHistory` extracts insert-shaped source values from a history row: the core history and metadata columns are stripped, and captured values are preserved exactly, including primary keys.\nIt never writes anything itself; compose it with your own tracked mutations so the restore is recorded in history like any other write.\n\n```ts\nimport { rowHistory, sourceValuesFromHistory } from '@bcap/drizzle-history';\n\n// Undelete: the `-` snapshot captured the row's final values, so re-insert them.\nconst [lastRow] = await rowHistory(db, users, { id: 42 }, { limit: 1 });\nawait trackedDb.insert(users).values(sourceValuesFromHistory(users, lastRow!));\n\n// Revert: update the live row back to an earlier snapshot.\nawait trackedDb\n\t.update(users)\n\t.set(sourceValuesFromHistory(users, earlierSnapshot))\n\t.where(eq(users.id, 42));\n```\n\nGenerated columns and always-generated identity columns are omitted from the extracted values because PostgreSQL computes those itself.\nColumns excluded from the history table (see \"Excluding columns\" below) were never captured, so they are also absent from the extracted values: an update-based revert leaves them untouched on the live row, and an undelete insert must supply them separately when the column is `NOT NULL` without a default.\n\n## Adopting @bcap/drizzle-history on an existing project\n\n### Migrations with drizzle-kit\n\nHistory tables are derived at runtime, so drizzle-kit would not normally see them.\n`defineConfigWithHistory` from the `@bcap/drizzle-history/kit` entrypoint wraps your existing drizzle-kit config and exposes each tracked table's companion history table to every schema-reading command:\n\n```ts\n// drizzle.config.ts\nimport { defineConfigWithHistory } from '@bcap/drizzle-history/kit';\n\nexport default defineConfigWithHistory(\n\t{\n\t\tdialect: 'postgresql',\n\t\tschema: './src/schema.ts',\n\t\tout: './drizzle',\n\t},\n\t__dirname,\n);\n```\n\nPass `__dirname` as the second argument so schema paths resolve relative to the config file.\ndrizzle-kit compiles config files to CJS internally, so `import.meta.dirname` is `undefined` in that context; `__dirname` is the reliable choice.\nAfter wrapping the config, `drizzle-kit generate` produces migrations that create and evolve the history tables alongside their sources.\n\n### Backfilling existing rows\n\n`populateHistory` writes an initial `+` snapshot for every existing source row, so point-in-time queries have a baseline:\n\n```ts\nimport { populateHistory } from '@bcap/drizzle-history';\n\nconst result = await populateHistory(db, [users, posts], { batchSize: 10_000 });\nconsole.log(result.totalRows);\n```\n\nThe backfill runs as `INSERT ... SELECT` statements where possible, deduplicates against already-populated rows by row identity (`skipPopulated` defaults to `true`, so reruns are safe), wraps the whole run inside a single transaction when the database supports it, and stamps every backfilled row with a single shared `historyDate`.\nToolkits created with `defineHistory` expose a typed `populateHistory` that requires the same metadata as tracked writes:\n\n```ts\nawait history.populateHistory(db, [articles], {\n\tmetadata: { editorId: 0, reason: 'initial backfill' },\n});\n```\n\n## Retention\n\n`pruneHistory` deletes old history rows according to a retention policy, with a `dryRun` mode that reports counts without deleting anything:\n\n```ts\nimport { pruneHistory } from '@bcap/drizzle-history';\n\n// Preview: how many rows would a real prune delete?\nconst preview = await pruneHistory(db, [users, posts], {\n\tolderThan: oneYearAgo,\n\tkeepLatest: 10,\n\tdryRun: true,\n});\n\n// Delete rows that are both older than the cutoff and outside the newest\n// ten snapshots for their row.\nconst result = await pruneHistory(db, [users, posts], {\n\tolderThan: oneYearAgo,\n\tkeepLatest: 10,\n});\nconsole.log(result.totalDeleted);\n```\n\n`olderThan` deletes rows recorded strictly before the cutoff, `keepLatest` keeps the newest N snapshots per logical row, and providing both deletes only rows that satisfy both conditions.\nPruning is irreversible and shrinks what `asOf` can reconstruct before the pruned horizon, so prefer a `dryRun` first.\n\n## Excluding columns\n\nColumns that should never be captured (secrets, large blobs) can be excluded from the history table entirely:\n\n```ts\nconst users = withHistory(\n\tpgTable('users', {\n\t\tid: serial().primaryKey(),\n\t\temail: text().notNull(),\n\t\tpasswordHash: text('password_hash').notNull(),\n\t}),\n\t{ exclude: ['passwordHash'] },\n);\n```\n\nExcluded columns are absent from the history table's DDL and select model, tracked writes and `populateHistory` never capture them, and derived indexes that reference them are skipped.\nRow identity columns cannot be excluded, because history rows must remain correlatable to their source rows.\n\n## What gets captured (and what does not)\n\n@bcap/drizzle-history captures mutations by proxying the drizzle database object, not by installing database triggers.\nThat keeps the database dependency-free, but it draws a hard boundary: only writes that flow through the wrapped database object's query builder are recorded.\nThe following writes bypass capture silently:\n\n- Raw SQL through `db.execute(...)`, even on the wrapped instance.\n- Writes through the original unwrapped `db` reference, or through transactions opened on it.\n- Writes from any other client or process, including cron jobs, `psql`, and admin tools.\n- Database-side changes: `ON DELETE CASCADE` and other foreign key actions, triggers, rules, and server-side procedures.\n- Schema changes applied by `drizzle-kit push` that modify or delete data.\n\nIf your write path is centralized in an application that can export a single wrapped database instance, this boundary is easy to hold.\nIf many writers touch the database directly, consider a trigger-based or CDC approach instead (see the comparison below); trigger-based capture is also on this project's roadmap.\nThe full list of limitations, each with its consequence and workaround, is in [docs/limitations.md](./docs/limitations.md).\n\n## Compatibility\n\n| Component   | Supported                                                                                                  |\n| ----------- | ---------------------------------------------------------------------------------------------------------- |\n| drizzle-orm | 0.45.x and 1.x (including 1.0.0 release candidates)                                                        |\n| drizzle-kit | 0.30 or 0.31 with drizzle-orm 0.45.x; 1.x with drizzle-orm 1.x (optional, for `@bcap/drizzle-history/kit`) |\n| PostgreSQL  | 13 and newer                                                                                               |\n| Drivers     | postgres-js, node-pg, PGlite, neon-http                                                                    |\n| Node.js     | 22 and newer, ESM and CJS                                                                                  |\n\nThe full test suite runs against both drizzle-orm release lines on every change, and the first `withHistory(db)` call validates the installed version at runtime.\nDetails, including driver result-shape handling and family-specific notes, are in [docs/compatibility.md](./docs/compatibility.md).\n\n## Comparison with alternatives\n\nEach of these approaches is the right tool for some systems; the differences are about where capture happens and what the history data looks like.\n\n**PostgreSQL audit triggers.**\nTriggers capture every write, including raw SQL, other clients, and cascades, which is their decisive advantage when many writers touch the database.\nThe tradeoffs are that capture logic lives in the database (trigger DDL to write, migrate, and keep in sync per table), and application-level context such as the acting user must be smuggled in through session settings.\n@bcap/drizzle-history keeps capture in the application, where attribution metadata is a typed, compiler-enforced function argument, at the cost of only seeing writes that flow through the wrapped client.\n\n**Generic JSONB audit logs (supa_audit and similar).**\nThese record row images as JSONB in one shared table, which requires no per-table DDL and tolerates schema drift.\nThe cost is that history rows are untyped documents: reconstructing typed rows, diffing, and indexing per-column all require JSONB operators and manual casting.\n@bcap/drizzle-history mirrors each source column as a real typed column, so history rows are ordinary drizzle select models and point-in-time queries are plain SQL over indexed columns.\n\n**Change data capture pipelines (Debezium, logical replication).**\nCDC observes the write-ahead log, so it captures everything with low overhead and can stream changes into warehouses and event systems.\nIt is operationally heavier (connectors, topics, consumers), history lands outside the source database, and attribution metadata is not naturally part of the stream.\nPrefer CDC when history must feed other systems; prefer @bcap/drizzle-history when you want queryable, attributed history inside the same database and transaction.\n\n## Roadmap\n\n- Trigger-based capture, so out-of-process writes and cascades can be recorded.\n- Transaction and revision grouping, so related writes share one logical change id.\n- No-op update suppression, so updates that change nothing record nothing.\n- Masked columns: capture that a value changed without storing the value.\n- Shared audit table mode as an alternative to one history table per source table.\n- A monotonic ordering tiebreaker (a sequence column or UUIDv7-style `historyId` values) with a compatible migration path, so multiple writes to one row inside a single transaction order deterministically.\n\n## How it works\n\n`withHistory(db)` intercepts exactly five properties of the drizzle database: `insert`, `update`, `delete`, `transaction`, and `batch` (which refuses history-tracked builders); everything else forwards untouched.\nMutation builders on tracked tables record each chained call, and execution replays the chain with a forced `RETURNING` of all source columns so the affected rows can be copied into the history table inside the same transaction.\nUpserts are classified per row using PostgreSQL's `xmax` system column, so `onConflictDoUpdate` records `+` for inserted rows and `~` for updated ones in a single statement.\nThe full design, including history table derivation rules, index naming, atomicity semantics, and the memory profile of large mutations, is described in [docs/how-it-works.md](./docs/how-it-works.md).\n\n## API summary\n\nThe complete reference, with options, error conditions, and examples for every export, is in [docs/api.md](./docs/api.md).\nPractical integration patterns (Next.js wiring, AsyncLocalStorage attribution, testing with PGlite, adoption, retention, and point-in-time debugging) are in [docs/recipes.md](./docs/recipes.md).\n\n| Export                                                                                | Purpose                                                                |\n| ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| `withHistory(table, options?)`                                                        | Derive a companion history table and mark the table as tracked.        |\n| `withHistory(db, options?)`                                                           | Wrap a drizzle database so tracked mutations record history.           |\n| `defineHistory({ metadataColumns })`                                                  | Create a toolkit whose tracked tables share required metadata columns. |\n| `createHistoryTable(table, options?)`                                                 | Derive a standalone history table without tracking.                    |\n| `asOf(db, table, timestamp, options?)`                                                | Reconstruct the table's rows as of a point in time.                    |\n| `rowHistory(db, table, identity, options?)`                                           | Fetch every snapshot of one logical row.                               |\n| `diffHistoryRows(table, older, newer, options?)`                                      | List the source columns that changed between two snapshots.            |\n| `sourceValuesFromHistory(table, historyRow)`                                          | Extract insert-shaped source values for undelete and revert flows.     |\n| `populateHistory(db, tables, options?)`                                               | Backfill initial `+` snapshots for existing source rows.               |\n| `pruneHistory(db, tables, options)`                                                   | Apply a retention policy, with `dryRun` preview.                       |\n| `isTrackedTable(value)` / `getTrackedTablePair(table)` / `expandTrackedTables(input)` | Introspect tracked tables and schema objects.                          |\n| `getHistoryTableMetadata(historyTable)` / `validateHistorySetup(tables)`              | Inspect and validate history table configuration.                      |\n| `checkDrizzleCompatibility()`                                                         | Assert the installed drizzle-orm version is supported.                 |\n| `defineConfigWithHistory(config, __dirname)` (from `@bcap/drizzle-history/kit`)       | Expose derived history tables to drizzle-kit commands.                 |\n\n## Contributing\n\nDevelopment uses Bun, and the entire test suite runs against in-memory PostgreSQL via PGlite, so no database setup is needed.\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for the everyday commands and expectations.\n\n## License\n\n[BSD-2-Clause](./LICENSE)\n","readmeFilename":"README.md"}