Audit Fields (created_at and updated_at)
Understanding when records were created and last modified is essential for debugging, data analysis, user experience (e.g., "last updated 5 minutes ago"), and synchronization in offline-first appli...
Context
Understanding when records were created and last modified is essential for debugging, data analysis, user experience (e.g., "last updated 5 minutes ago"), and synchronization in offline-first applications. Without standardized timestamp tracking, teams forget to add timestamps to new tables, implement them inconsistently with different names or types, and lose visibility into record history. For offline-first applications, timestamp fields are critical for conflict resolution during synchronization (e.g., last-write-wins strategies) and for determining data freshness.
Decision
Every database table must include created_at and updated_at audit fields. These fields are stored as Unix INTEGER timestamps in SQLite and are automatically populated using Drizzle ORM's dynamic column features.
created_at: Set automatically on insert via.$defaultFn(() => new Date()). Must be NOT NULL.updated_at: Set automatically on update via.$onUpdateFn(() => new Date()). Nullable (only set when the record is updated).- Field names follow snake_case in the database (
created_at,updated_at) and camelCase in TypeScript code (createdAt,updatedAt). - These fields must not be manually set in application code under normal circumstances. Drizzle manages them automatically.
Do's and Don'ts
Do
- Include
createdAt/created_atandupdatedAt/updated_aton every table. - Use
.$defaultFn(() => new Date())for automaticcreatedAtpopulation on insert. - Use
.$onUpdateFn(() => new Date())for automaticupdatedAtpopulation on update. - Store timestamps as
integerwith{ mode: "timestamp" }in SQLite. - Make
createdAtNOT NULL (always required). - Make
updatedAtnullable (only set on updates). - Use these fields for sync conflict resolution in offline-first scenarios.
- Add indexes on timestamp fields if used for filtering or sorting.
Don't
- Create tables without
createdAtandupdatedAtfields. - Use different field names (e.g.,
dateCreated,timestamp,modified). - Manually set these fields in application code. Let Drizzle handle them.
- Use string or text types for timestamp storage.
- Rely on these fields for business logic ordering. Use dedicated domain-specific fields.
- Forget to migrate existing tables to include these fields.
Consequences
Positive
- Consistent auditing capability across all tables.
- Always know when records were created and last modified, aiding debugging and troubleshooting.
- Timestamps enable conflict resolution for offline-first synchronization.
- Can display user-facing relative time (e.g., "created 5 minutes ago") for any record.
- Automatic management via Drizzle eliminates manual timestamp bookkeeping.
- Minimal storage and performance overhead (two INTEGER columns per table).
Negative
- Two additional columns per table add slight schema complexity.
- Existing tables require a migration to add the fields.
- Only tracks create and update times, not who made the change or why.
- Client and server clocks may differ, which can affect sync conflict resolution accuracy.