Skip to content

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

CategoryDefinitionExample
InstantA point in time (date + time + timezone offset). Stored as UTC.2026-02-23T04:56:22.000Z
Date-onlyA calendar date without time component.2026-02-23
MonthA calendar month.2026-02
YearA calendar year.2026
PeriodA pair of instants or dates representing a range.{ start, end }
DurationA length of time, not anchored to a specific point.PT30M (30 minutes), P7D (7 days)

2. Storage Rules

2.1 Instants (most common)

RuleSpec
Storage typeDate (MongoDB native / Mongoose)
TimezoneAlways UTC — no local timezone in DB
Sourcenew Date() on server, OR parsed from client ISO-8601 input
MongooseUse timestamps: true for createdAt / updatedAt (auto-UTC)

2.2 Date-only

RuleSpec
Storage typeString — format YYYY-MM-DD
NOT DateStoring date-only as Date causes timezone drift (midnight UTC ≠ midnight local)
When to usebirthDate, invoiceDueDate, reportMonth — user means "this calendar day"

2.3 Month / Year

RuleSpec
MonthString — format YYYY-MM
YearNumber — 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"
RuleSpec
FormatYYYY-MM-DDTHH:mm:ss.sssZ
TimezoneAlways Z (UTC) — client converts to local
PrecisionMilliseconds (3 digits)
ImplementationMongoose 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 published

4. API Input Rules

4.1 Instants

Client sendsServer 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 sendsServer 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 sendsServer accepts
"2026-02"✅ Month
2026✅ Year

5. Field Naming Conventions

5.1 Suffix Rules

SuffixSemanticsTypeExample
*AtSystem-controlled instant (audit/lifecycle)Date (UTC)createdAt, updatedAt, deletedAt, publishedAt, processedAt, validatedAt
*DateBusiness-controlled date range boundaryDate (UTC)startDate, endDate, expectedDate, dueDate
*OnUser-provided date-only (calendar date, no time)String (YYYY-MM-DD)birthOn, invoiceDueOn
*MonthUser-provided monthString (YYYY-MM)reportMonth, billingMonth
*YearUser-provided yearNumberfiscalYear

5.2 Key Rules

  1. *At = server instant — NEVER accept from client (see AUDIT_STAMPING_STANDARD)
  2. *Date = business instant — accepted from client as ISO-8601, used for scheduling/periods
  3. *On = date-only — accepted from client as YYYY-MM-DD string
  4. System uses *At for automatic lifecycle stamps; *Date for user-provided ranges

5.3 ❌ Forbidden Suffixes

ForbiddenWhyCorrect
startAt / endAt on business fields*At implies server-controlledstartDate / endDate
createdDate / updatedDate*Date implies user-controlledcreatedAt / updatedAt
deliveryTimeAmbiguous — instant or duration?deliveredAt (instant) or deliveryDuration (duration)
date (bare)Too genericorderDate, birthOn, etc.

6. Period Fields

For entities with date ranges (voucher validity, subscription period, event times):

typescript
// 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

typescript
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

typescript
// 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

PolicyRule
Server always operates in UTCprocess.env.TZ = undefined (default Node.js UTC)
Database stores UTC onlyMongoose Date = UTC
API outputs UTC onlyNo timezone conversion on server
Client converts to localFrontend uses Intl.DateTimeFormat or dayjs
Admin displayShow 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

DateChange
2026-02-23Initial creation — definitions, storage rules, API I/O, naming, NestJS patterns

FitZalo Platform Documentation