Nestjs Drizzle
Guides

Timestamps

Package-managed vs schema and DB-managed created_at and updated_at, and why DB-managed is recommended.

The package can automatically stamp created_at and updated_at on every write, or you can let your database manage them. The two approaches are mutually exclusive. Pick one per project.

Package-managed (convenient)

app.module.ts
DrizzleCrudModule.forRoot({
  dialect: 'postgresql',
  connectionString: process.env.DATABASE_URL,
  schema,
  defaults: { timestamps: true }, // default
});

The package uses the application clock and stamps columns only on writes that go through the package:

  • create() sets both created_at and updated_at.
  • update(), softDelete() and restore() bump updated_at.
  • update() with nothing to change is a 400. It does not bump updated_at on its own.

The settings name the keys in your Drizzle table object, created_at / updated_at by default. A camelCase schema (updatedAt: timestamp('updated_at')) needs them set per entity:

config: { timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }

Why does my service fail at startup with a timestamps error?

Because a configured key is not on the table. Before 3.3.1 the package wrote updated_at into the values, Drizzle ignored the unknown key, and updatedAt silently never moved. The service now checks both keys when it is built and stops with a message that names the key to use. Set the keys as above, or turn timestamps off for that entity with timestamps: undefined (or project wide with defaults: { timestamps: false }).

Application clock, partial coverage

Package-managed timestamps use the application server's clock, not the database's. They also only apply to writes through the package. Raw SQL, migrations, and other code paths won't set them. For authoritative, wall-clock-accurate, all-write-path timestamps, use schema/DB-managed.

For authoritative timestamps (database time, on every write path), define them in your Drizzle schema and leave the package's timestamps disabled:

db/schema.ts
import { pgTable, serial, varchar, timestamp } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 100 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),

  createdAt: timestamp('created_at', { withTimezone: true })
    .defaultNow()
    .notNull(),

  updatedAt: timestamp('updated_at', { withTimezone: true })
    .defaultNow()
    .notNull()
    .$onUpdate(() => new Date()),

  deletedAt: timestamp('deleted_at', { withTimezone: true }),
});
  • defaultNow() lets the database set created_at on insert.
  • $onUpdate(() => new Date()) makes Drizzle bump updated_at on every update, including the package's own update().

For updated_at that's authoritative even for raw SQL (e.g. a script that runs UPDATE users SET name = ... directly), add a Postgres trigger or a MySQL ON UPDATE CURRENT_TIMESTAMP clause:

-- Postgres
CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_set_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- MySQL
ALTER TABLE users
  MODIFY updated_at TIMESTAMP NOT NULL
    DEFAULT CURRENT_TIMESTAMP
    ON UPDATE CURRENT_TIMESTAMP;

Then turn package timestamps off

app.module.ts
DrizzleCrudModule.forRoot({
  dialect: 'postgresql',
  connectionString: process.env.DATABASE_URL,
  schema,
  defaults: { timestamps: false },
});

The package will not touch the timestamp columns. The database owns them.

Comparison

AspectPackage-managedDB-managed
Clock sourceApplication serverDatabase
CoverageOnly package writesAll writes (including raw SQL)
Test clock controlTrivial (mock Date.now())Requires DB time mocking
Setupdefaults: { timestamps: true }Drizzle defaultNow() / $onUpdate() (or triggers)
Source of truthLess authoritativeMore authoritative

When package-managed is the right choice

  • Tests. You can mock the application clock for deterministic timestamps.
  • Single-writer apps. Every write goes through the package, so coverage isn't an issue.
  • Time zone handling is the app's job. You've already wrapped your DB driver to use UTC.

When DB-managed is the right choice (default recommendation)

  • Multiple writers. Other services, scripts, manual SQL.
  • Multi-region / replicated databases. You want a single clock source.
  • Production. Give the database the last word on time.

Next

On this page