Time & Date Standard — FitZalo V2
Governance ID: GOV-TIME | Owner: API Governance Lead Scope: All modules, all endpoints, all schemas Effective: 2026-02-23 | Status: APPROVED
1. Definitions
| Category | Definition | Example |
|---|---|---|
| Instant | A point in time (date + time + timezone offset). Stored as UTC. | 2026-02-23T04:56:22.000Z |
| Date-only | A calendar date without time component. | 2026-02-23 |
| Month | A calendar month. | 2026-02 |
| Year | A calendar year. | 2026 |
| Period | A pair of instants or dates representing a range. | { start, end } |
| Duration | A length of time, not anchored to a specific point. | PT30M (30 minutes), P7D (7 days) |
2. Storage Rules
2.1 Instants (most common)
| Rule | Spec |
|---|---|
| Storage type | Date (MongoDB native / Mongoose) |
| Timezone | Always UTC — no local timezone in DB |
| Source | new Date() on server, OR parsed from client ISO-8601 input |
| Mongoose | Use timestamps: true for createdAt / updatedAt (auto-UTC) |
2.2 Date-only
| Rule | Spec |
|---|---|
| Storage type | String — format YYYY-MM-DD |
NOT Date | Storing date-only as Date causes timezone drift (midnight UTC ≠ midnight local) |
| When to use | birthDate, invoiceDueDate, reportMonth — user means "this calendar day" |
2.3 Month / Year
| Rule | Spec |
|---|---|
| Month | String — format YYYY-MM |
| Year | Number — e.g. 2026 |
3. API Output Rules (Canonical Format)
3.1 Instants → ISO-8601 UTC with Z suffix
"createdAt": "2026-02-23T04:56:22.000Z"
"updatedAt": "2026-02-23T05:12:33.123Z"| Rule | Spec |
|---|---|
| Format | YYYY-MM-DDTHH:mm:ss.sssZ |
| Timezone | Always Z (UTC) — client converts to local |
| Precision | Milliseconds (3 digits) |
| Implementation | Mongoose Date → JS Date.toJSON() → automatic ISO-8601 |
Why UTC? The client knows the user's timezone and renders accordingly. Storing/outputting in UTC avoids ambiguity when users span timezones within a single tenant.
3.2 Date-only → String YYYY-MM-DD
"birthDate": "1990-05-15"
"invoiceDueDate": "2026-03-01"3.3 Month → String YYYY-MM
"reportMonth": "2026-02"3.4 Null / Empty
"deletedAt": null // never deleted
"publishedAt": null // not yet published4. API Input Rules
4.1 Instants
| Client sends | Server accepts |
|---|---|
"2026-02-23T04:56:22.000Z" | ✅ ISO-8601 UTC |
"2026-02-23T11:56:22+07:00" | ✅ ISO-8601 with offset (server converts to UTC) |
"2026-02-23T04:56:22" | ✅ Treated as UTC (no offset = UTC assumed) |
1708660582000 (epoch ms) | ❌ FORBIDDEN — use ISO-8601 string only |
Validation: Use @IsISO8601() from class-validator on all instant fields in DTOs.
4.2 Date-only
| Client sends | Server accepts |
|---|---|
"2026-02-23" | ✅ YYYY-MM-DD format |
"23/02/2026" | ❌ FORBIDDEN — ambiguous |
"Feb 23, 2026" | ❌ FORBIDDEN |
Validation: Use @Matches(/^\d{4}-\d{2}-\d{2}$/) or custom @IsDateOnly().
4.3 Month / Year
| Client sends | Server accepts |
|---|---|
"2026-02" | ✅ Month |
2026 | ✅ Year |
5. Field Naming Conventions
5.1 Suffix Rules
| Suffix | Semantics | Type | Example |
|---|---|---|---|
*At | System-controlled instant (audit/lifecycle) | Date (UTC) | createdAt, updatedAt, deletedAt, publishedAt, processedAt, validatedAt |
*Date | Business-controlled date range boundary | Date (UTC) | startDate, endDate, expectedDate, dueDate |
*On | User-provided date-only (calendar date, no time) | String (YYYY-MM-DD) | birthOn, invoiceDueOn |
*Month | User-provided month | String (YYYY-MM) | reportMonth, billingMonth |
*Year | User-provided year | Number | fiscalYear |
5.2 Key Rules
*At= server instant — NEVER accept from client (see AUDIT_STAMPING_STANDARD)*Date= business instant — accepted from client as ISO-8601, used for scheduling/periods*On= date-only — accepted from client asYYYY-MM-DDstring- System uses
*Atfor automatic lifecycle stamps;*Datefor user-provided ranges
5.3 ❌ Forbidden Suffixes
| Forbidden | Why | Correct |
|---|---|---|
startAt / endAt on business fields | *At implies server-controlled | startDate / endDate |
createdDate / updatedDate | *Date implies user-controlled | createdAt / updatedAt |
deliveryTime | Ambiguous — instant or duration? | deliveredAt (instant) or deliveryDuration (duration) |
date (bare) | Too generic | orderDate, birthOn, etc. |
6. Period Fields
For entities with date ranges (voucher validity, subscription period, event times):
// Schema
@Prop({ required: true }) startDate!: Date; // business-controlled instant
@Prop({ required: true }) endDate!: Date; // business-controlled instant
// DTO input
@IsISO8601() startDate: string;
@IsISO8601() endDate: string;
// Validate: startDate < endDate
// API output
"startDate": "2026-03-01T00:00:00.000Z"
"endDate": "2026-03-31T23:59:59.999Z"7. NestJS Implementation Patterns
7.1 DTO Validation
import { IsISO8601, IsOptional, Matches } from 'class-validator';
// Instant (business date range)
@IsISO8601() startDate: string;
// Date-only
@Matches(/^\d{4}-\d{2}-\d{2}$/, { message: 'Must be YYYY-MM-DD format' })
birthOn: string;
// Month
@Matches(/^\d{4}-\d{2}$/, { message: 'Must be YYYY-MM format' })
reportMonth: string;7.2 Response Serialization
Mongoose Date is serialized by JSON.stringify as ISO-8601 UTC automatically. No custom serializer needed — rely on default Mongoose/JSON behavior.
7.3 Query Parsing
// Controller: parse date range from query params
@Query('from') from?: string,
@Query('to') to?: string,
// Service: convert to Date for MongoDB query
const filter: any = { tenantId };
if (from) filter.createdAt = { ...filter.createdAt, $gte: new Date(from) };
if (to) filter.createdAt = { ...filter.createdAt, $lte: new Date(to) };8. Timezone Policy
| Policy | Rule |
|---|---|
| Server always operates in UTC | process.env.TZ = undefined (default Node.js UTC) |
| Database stores UTC only | Mongoose Date = UTC |
| API outputs UTC only | No timezone conversion on server |
| Client converts to local | Frontend uses Intl.DateTimeFormat or dayjs |
| Admin display | Show UTC + user's local time side-by-side if needed |
FORBIDDEN: Server-side conversion to local timezone. The server MUST NOT format dates in any timezone other than UTC. Client-side rendering handles localization.
Changelog
| Date | Change |
|---|---|
| 2026-02-23 | Initial creation — definitions, storage rules, API I/O, naming, NestJS patterns |