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)
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 bothcreated_atandupdated_at.update()andsoftDelete()bumpupdated_at.restore()does not touch timestamps.
Column names default to created_at / updated_at. Override per entity:
config: { timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }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.
Schema/DB-managed (recommended)
For authoritative timestamps (database time, on every write path), define them in your Drizzle schema and leave the package's timestamps disabled:
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 setcreated_aton insert.$onUpdate(() => new Date())makes Drizzle bumpupdated_aton every update, including the package's ownupdate().
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
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
| Aspect | Package-managed | DB-managed |
|---|---|---|
| Clock source | Application server | Database |
| Coverage | Only package writes | All writes (including raw SQL) |
| Test clock control | Trivial (mock Date.now()) | Requires DB time mocking |
| Setup | defaults: { timestamps: true } | Drizzle defaultNow() / $onUpdate() (or triggers) |
| Source of truth | Less authoritative | More 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.