Timezone Management
This guide explains how the system handles timezone management to ensure accurate date/time operations across the entire application stack. The system accepts dates in the client’s local timezone, processes them correctly, but stores and transmits data in UTC format.
The Problem: Timezone Mismatch
Section titled “The Problem: Timezone Mismatch”In enterprise systems, timezone misalignment causes critical issues:
Scenario
Section titled “Scenario”- Server/Database Time: Configured as UTC (GMT+0) for international standardization
- Business Logic (Local Time): Day cutoff (e.g., “today’s records”) must start at 00:00 in the user’s timezone
- The Offset Gap Problem:
- Query: “Find records for 2024-01-15”
- Database interprets as:
2024-01-15 00:00:00 UTC - In a GMT+7 timezone, that’s:
2024-01-15 07:00:00 +07:00 - Result: Data from 00:00–06:59 local time is missing from the query
graph LR
subgraph "User Request (Local Time)"
A["2024-01-15<br/>(Full day locally)"]
end
subgraph "Without Timezone Handling"
B["2024-01-15 00:00 UTC<br/>(07:00 Local)"]
C["Missing: 00:00-06:59 Local"]
end
subgraph "With Timezone Handling"
D["2024-01-14 17:00 UTC<br/>(00:00 Local)"]
E["2024-01-15 16:59 UTC<br/>(23:59 Local)"]
end
A --> B
B --> C
A --> D
D --> E
style C fill:#ffcccc
style D fill:#ccffcc
style E fill:#ccffcc
Architecture: 3-Layer Solution
Section titled “Architecture: 3-Layer Solution”A “Double-Lock” approach ensures correct timezone handling at every level:
graph TB
subgraph "Layer 1: Application Bootstrap"
A1["process.env.TZ = configured timezone"]
A2["dayjs.tz.setDefault(appTimezone)"]
end
subgraph "Layer 2: Database Connection"
B1["TypeORM: timezone: 'Z'"]
B2["All queries use UTC"]
end
subgraph "Layer 3: Query Builder"
C1["Date-only expansion"]
C2["Timezone conversion to UTC"]
end
A1 --> A2
A2 --> B1
B1 --> B2
B2 --> C1
C1 --> C2
Layer 1: Application Bootstrap
Section titled “Layer 1: Application Bootstrap”Setting Up Dayjs in Bootstrap
Section titled “Setting Up Dayjs in Bootstrap”In the application bootstrap (main.ts or bootstrap-application.ts), configure the global timezone settings:
import dayjs from 'dayjs';import timezone from 'dayjs/plugin/timezone';import utc from 'dayjs/plugin/utc';
export async function bootstrapApplication(options: BootstrapOptions) { // 1. Force Node.js process to use the configured application timezone // This affects new Date() and other native date operations process.env.TZ = process.env.APP_TIMEZONE ?? 'UTC';
// 2. Configure Dayjs with timezone support dayjs.extend(utc); dayjs.extend(timezone); dayjs.tz.setDefault(process.env.APP_TIMEZONE ?? 'UTC');
// ... rest of bootstrap configuration}Why Both Settings?
Section titled “Why Both Settings?”| Setting | Purpose | Affects |
|---|---|---|
process.env.TZ | Sets Node.js process timezone | new Date(), Date.now(), native date operations |
dayjs.tz.setDefault() | Sets dayjs default timezone | All dayjs operations without explicit timezone |
Layer 2: Database Connection
Section titled “Layer 2: Database Connection”TypeORM Configuration
Section titled “TypeORM Configuration”In the DatabaseModule, configure TypeORM to communicate with PostgreSQL using UTC:
@Module({})export class DatabaseModule { static registerAsync(connectionName: string): DynamicModule { return { module: DatabaseModule, imports: [ TypeOrmModule.forRootAsync({ name: connectionName, useFactory: (configService: ConfigService) => ({ type: 'postgres', replication: { master: { /* master config */ }, slaves: [{ /* replica config */ }], }, // CRITICAL: Force driver to communicate in UTC timezone: 'Z', // ... other options }), inject: [ConfigService], }), ], }; }}PostgreSQL Column Types
Section titled “PostgreSQL Column Types”Always use timestamptz (timestamp with time zone) for date/time columns:
// Entity definition@Entity({ name: 'records', database: AppDatabases.APP_CORE })export class Record implements ITimestamp { @PrimaryGeneratedColumn('uuid') id: string;
// ✅ CORRECT: Use timestamptz for proper timezone handling @Column({ type: 'timestamptz', comment: 'Record date and time' }) record_date: Date;
// ✅ CORRECT: Audit columns with timezone @CreateDateColumn({ type: 'timestamptz' }) created_at: Date;
@UpdateDateColumn({ type: 'timestamptz' }) updated_at: Date;
// ❌ AVOID: timestamp without timezone (loses timezone info) // @Column({ type: 'timestamp' }) // some_date: Date;}Layer 3: Query Builder Date Processing
Section titled “Layer 3: Query Builder Date Processing”How TypeOrmQueryBuilder Handles Dates
Section titled “How TypeOrmQueryBuilder Handles Dates”The TypeOrmQueryBuilder class automatically processes date values based on the query context:
/** * Date processing logic: * - Date-only values (YYYY-MM-DD) are expanded to cover the full day * - Greater than (>=): Start of day (00:00:00) in user timezone → UTC * - Less than (<=): End of day (23:59:59.999) in user timezone → UTC */private processDateValue(value: unknown, direction: ComparisonDirection): unknown { if (typeof value !== 'string') return value;
const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(value);
if (isDateOnly) { return this.expandDateOnlyValue(value, direction); }
// Full datetime: convert directly to UTC if (dayjs(value).isValid()) { return dayjs.tz(value, this.timezone).toISOString(); }
return value;}
private expandDateOnlyValue(dateString: string, direction: ComparisonDirection): string { const localDate = dayjs.tz(dateString, this.timezone);
switch (direction) { case 'greater': return localDate.startOf('day').toISOString(); case 'less': return localDate.endOf('day').toISOString(); case 'equal': default: return localDate.startOf('day').toISOString(); }}Date Expansion Examples
Section titled “Date Expansion Examples”| Input | Direction | Local Time (UTC+7 example) | UTC Output |
|---|---|---|---|
2024-01-15 | greater | 2024-01-15 00:00:00 +07:00 | 2024-01-14T17:00:00.000Z |
2024-01-15 | less | 2024-01-15 23:59:59.999 +07:00 | 2024-01-15T16:59:59.999Z |
2024-01-15 14:30:00 | any | 2024-01-15 14:30:00 +07:00 | 2024-01-15T07:30:00.000Z |
Input Validation: Standard vs. Strict DateTime Validation
Section titled “Input Validation: Standard vs. Strict DateTime Validation”When working with timestamptz columns and receiving date/time data from client payloads (HTTP POST, PUT, PATCH), proper input validation is critical. Without strict validation, timezone ambiguity can cause data storage issues and query mismatches.
The Problem: Timezone Ambiguity in Payloads
Section titled “The Problem: Timezone Ambiguity in Payloads”Consider this scenario:
- Client A (UTC+7) sends:
"2024-01-15T09:00:00"(no timezone) - Client B (UTC-5) sends:
"2024-01-15T09:00:00"(no timezone) - Question: What time should be stored in the database?
Without timezone information, the backend cannot determine the user’s intended time. If the server interprets both as UTC:
- Client A expected:
2024-01-15T02:00:00Z(09:00 local time) - Client B expected:
2024-01-15T14:00:00Z(09:00 local time) - Server stored:
2024-01-15T09:00:00Z(wrong for both!)
Result: Data inconsistency, failed queries, and “missing data” bugs.
graph LR
subgraph "Client Sends (No Timezone)"
A1["2024-01-15T09:00:00"]
end
subgraph "Backend Interpretation"
B1["Assume UTC?<br/>Assume Server TZ?<br/>❌ Ambiguous"]
end
subgraph "Correct Approach"
C1["2024-01-15T09:00:00+07:00<br/>✅ Unambiguous"]
end
A1 --> B1
C1 --> D1["Store as UTC:<br/>2024-01-15T02:00:00Z"]
style B1 fill:#ffcccc
style D1 fill:#ccffcc
Comparison: IsDateString() vs. IsISO8601()
Section titled “Comparison: IsDateString() vs. IsISO8601()”| Feature | IsDateString() (Standard) | @IsISO8601() (Strict) |
|---|---|---|
| Source | class-validator built-in | Custom decorator |
| Accepts | Various date formats | Only complete ISO 8601 |
| Timezone Required | ❌ No | ✅ Yes |
| Validation Strictness | Loose | Strict |
| Recommended For | Display/filter inputs | Mutation payloads (POST/PUT/PATCH) |
Standard Validation: IsDateString()
Section titled “Standard Validation: IsDateString()”The built-in IsDateString() from class-validator accepts various date formats:
// ❌ All of these pass IsDateString() validation'2024-01-15'; // Date only - NO timezone'2024-01-15T09:00:00'; // Missing timezone offset'2024-01-15T09:00:00.000'; // Missing timezone offset'2024-01-15T09:00:00Z'; // ✅ Has timezone (UTC)'2024-01-15T09:00:00+07:00';// ✅ Has timezone offsetWhy this is problematic for mutations:
// DTO with standard validationexport class CreateRecordDTO { @IsDateString() // ❌ Accepts ambiguous formats record_date: string;}
// Client sends (missing timezone)POST /records{ "record_date": "2024-01-15T09:00:00" }
// Backend receives: ambiguous — what timezone was intended?Strict Validation: @IsISO8601() (Custom Decorator)
Section titled “Strict Validation: @IsISO8601() (Custom Decorator)”The custom @IsISO8601() decorator enforces complete ISO 8601 format with mandatory timezone offset:
/** * Strict Regex for ISO 8601 with mandatory Timezone Offset. * Pattern: YYYY-MM-DDTHH:mm:ss.sssZ or YYYY-MM-DDTHH:mm:ss.sss+HH:mm */private readonly ISO_8601_STRICT_REGEX = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;Validation Examples:
// ✅ Valid (complete format with timezone)'2024-01-15T09:00:00Z'; // UTC'2024-01-15T09:00:00+07:00'; // UTC+7'2024-01-15T09:00:00.000+07:00'; // With milliseconds'2024-01-15T09:00:00-05:00'; // UTC-5
// ❌ Invalid (rejected by @IsISO8601)'2024-01-15'; // Date only'2024-01-15T09:00:00'; // Missing timezone'2024-01-15 09:00:00'; // Wrong separator (space)'Jan 15, 2024'; // Wrong formatUsing @IsISO8601() in DTOs
Section titled “Using @IsISO8601() in DTOs”For any DTO that receives timestamptz data via POST, PUT, or PATCH, use the @IsISO8601() decorator:
import { IsISO8601 } from '@lib/common';import { IsOptional, IsString, IsUUID } from 'class-validator';
export class CreateRecordDTO { @IsUUID() resource_id: string;
@IsISO8601() // ✅ Strict validation - requires timezone record_date: string;
@IsISO8601() @IsOptional() scheduled_time?: string;
@IsString() record_type: string;}Client Request (Correct Format):
{ "resource_id": "uuid-123", "record_date": "2024-01-15T09:00:00+07:00", "record_type": "standard"}Client Request (Invalid — Will Be Rejected):
{ "resource_id": "uuid-123", "record_date": "2024-01-15T09:00:00", "record_type": "standard"}Error Response:
{ "status": { "code": 400001, "message": "Validation Failed" }, "errors": [ { "field": "record_date", "message": "record_date must be a complete ISO 8601 string with a timezone offset (e.g., 2024-01-15T14:30:00+07:00 or 2024-01-15T14:30:00Z)" } ]}When to Use Each Validation Approach
Section titled “When to Use Each Validation Approach”graph TD
A[Date/Time Field] --> B{Operation Type?}
B -->|POST, PUT, PATCH| C[Mutation Request]
B -->|GET Query Filter| D[Read Request]
C --> E["Use @IsISO8601()<br/>✅ Strict validation<br/>✅ Requires timezone"]
D --> F["TypeORM Query Builder<br/>✅ Auto-handles timezone<br/>✅ Accepts date-only format"]
E --> G["Backend stores UTC<br/>No ambiguity"]
F --> H["Converts to UTC<br/>Based on timezone param"]
style E fill:#ccffcc
style F fill:#ccffcc
HTTP Mutations (POST, PUT, PATCH): Use @IsISO8601()
Section titled “HTTP Mutations (POST, PUT, PATCH): Use @IsISO8601()”When saving data to timestamptz columns, always require complete ISO 8601 with timezone:
| Situation | Why Strict Validation? |
|---|---|
| Creating appointments | Exact time matters for scheduling |
| Updating record dates | Prevents data drift from timezone confusion |
| Recording event timestamps | Critical for auditability |
| Audit timestamps | Compliance requires precision |
HTTP Queries (GET with filters): Query Builder Handles It
Section titled “HTTP Queries (GET with filters): Query Builder Handles It”When filtering data via GET requests, the TypeORM Query Builder automatically handles timezone conversion (see Base Operations Architecture):
# These all work correctly via the query builderGET /records?filter=record_date||$eq||2024-01-15GET /records?filter=record_date||$between||2024-01-01,2024-01-31GET /records?s={"record_date":{"between":["2024-01-15","2024-01-20"]}}The query builder:
- Interprets date-only values in the configured timezone
- Expands to full day range (00:00:00 to 23:59:59.999)
- Converts to UTC for database query
Frontend Implementation Guide
Section titled “Frontend Implementation Guide”Ensure your frontend always sends complete ISO 8601 strings with timezone:
// ✅ With dayjs for explicit timezone handlingimport dayjs from 'dayjs';import timezone from 'dayjs/plugin/timezone';import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);dayjs.extend(timezone);
// ✅ Correct: Frontend sends complete ISO 8601const createRecord = async (resourceId: string, recordDate: Date) => { const response = await fetch('/api/v1/records', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resource_id: resourceId, // toISOString() always returns UTC with 'Z' suffix record_date: recordDate.toISOString(), }), }); return response.json();};
// For user-input local dates, explicitly set timezoneconst scheduleRecord = async (date: string, time: string, tz: string) => { // User input: date="2024-01-15", time="09:00", tz="Asia/Tokyo" const datetime = dayjs.tz(`${date} ${time}`, tz);
const response = await fetch('/api/v1/records', { method: 'POST', body: JSON.stringify({ scheduled_time: datetime.toISOString(), // UTC }), });};Summary: Validation Strategy
Section titled “Summary: Validation Strategy”| HTTP Method | Use Case | Validation | Format Accepted |
|---|---|---|---|
| POST | Create record | @IsISO8601() | Complete ISO 8601 with TZ |
| PUT | Replace record | @IsISO8601() | Complete ISO 8601 with TZ |
| PATCH | Update fields | @IsISO8601() | Complete ISO 8601 with TZ |
| GET | Filter/Query | Query Builder | Date-only or ISO 8601 |
This dual approach ensures:
- ✅ Data integrity: Mutations store precise, unambiguous timestamps
- ✅ Developer experience: Queries remain flexible and user-friendly
- ✅ International support: Works correctly across all timezones
Frontend Query Examples
Section titled “Frontend Query Examples”Using the timezone Query Parameter
Section titled “Using the timezone Query Parameter”The API supports an optional timezone query parameter to override the default:
# Default timezone (application-configured)GET /data-owner-bc/v1/records?filter=record_date||$eq||2024-01-15
# Custom timezoneGET /data-owner-bc/v1/records?filter=record_date||$eq||2024-01-15&timezone=America/New_York
# UTCGET /data-owner-bc/v1/records?filter=record_date||$eq||2024-01-15&timezone=UTCDate Filter Examples
Section titled “Date Filter Examples”Using filter Parameter (Recommended)
Section titled “Using filter Parameter (Recommended)”# Exact date (expands to full day in configured timezone)?filter=record_date||$eq||2024-01-15
# Greater than or equal (from start of day)?filter=record_date||$gte||2024-01-15
# Less than or equal (to end of day)?filter=record_date||$lte||2024-01-15
# Date range (between)?filter=record_date||$between||2024-01-01,2024-01-31
# With custom timezone?filter=record_date||$between||2024-01-01,2024-01-31&timezone=America/New_YorkUsing s Parameter (JSON Syntax)
Section titled “Using s Parameter (JSON Syntax)”# Exact date?s={"record_date":"2024-01-15"}
# Greater than?s={"record_date":{">":"2024-01-15"}}
# Date range?s={"record_date":{"between":["2024-01-01","2024-01-31"]}}
# Combined with timezone?s={"record_date":{"between":["2024-01-01","2024-01-31"]}}&timezone=Asia/TokyoDayjs Usage Guide
Section titled “Dayjs Usage Guide”Basic Usage in Services
Section titled “Basic Usage in Services”import dayjs from 'dayjs';import timezone from 'dayjs/plugin/timezone';import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);dayjs.extend(timezone);
const APP_TIMEZONE = process.env.APP_TIMEZONE ?? 'UTC';
@Injectable()export class RecordsService { /** * Get records for a specific date in the application timezone */ async getRecordsForDate(date: string, tz = APP_TIMEZONE): Promise<Record[]> { const localDate = dayjs.tz(date, tz);
const startOfDay = localDate.startOf('day').toISOString(); const endOfDay = localDate.endOf('day').toISOString();
return this.recordRepository.find({ where: { record_date: Between(startOfDay, endOfDay), }, }); }
/** * Create a record — @IsISO8601() ensures timezone is already included. * * The record_date is guaranteed to be a complete ISO 8601 string * with timezone (e.g., "2024-01-15T09:00:00+07:00"). * No manual timezone handling needed — just parse directly. */ async createRecord(dto: CreateRecordDTO): Promise<Record> { const record = this.recordRepository.create({ ...dto, // ✅ Simply parse — @IsISO8601() guarantees timezone is included record_date: dayjs(dto.record_date).toDate(), });
return this.recordRepository.save(record); }}Common Dayjs Operations
Section titled “Common Dayjs Operations”import dayjs from 'dayjs';
const TZ = 'Asia/Tokyo'; // Example timezone
// Current time in configured timezoneconst now = dayjs().tz(TZ);console.log(now.format('YYYY-MM-DD HH:mm:ss')); // "2024-01-15 14:30:00"
// Convert to UTC for database storageconst utcTime = now.utc();console.log(utcTime.toISOString()); // "2024-01-15T05:30:00.000Z"
// Start/End of day in local timezone → UTCconst localDate = dayjs.tz('2024-01-15 09:00:00', TZ);
const startOfDay = localDate.startOf('day');console.log(startOfDay.toISOString()); // UTC equivalent
const endOfDay = localDate.endOf('day');console.log(endOfDay.toISOString()); // UTC equivalent
// Add/subtract timeconst tomorrow = now.add(1, 'day');const lastWeek = now.subtract(7, 'day');
// Compare datesconst isAfter = dayjs('2024-01-15').isAfter('2024-01-10'); // trueconst isBefore = dayjs('2024-01-15').isBefore('2024-01-20'); // trueconst isSame = dayjs('2024-01-15').isSame('2024-01-15', 'day'); // trueFrontend Integration
Section titled “Frontend Integration”TypeScript/JavaScript Client
Section titled “TypeScript/JavaScript Client”import dayjs from 'dayjs';import timezone from 'dayjs/plugin/timezone';import utc from 'dayjs/plugin/utc';
dayjs.extend(utc);dayjs.extend(timezone);
const DEFAULT_TIMEZONE = 'UTC'; // Override per deployment
/** * Format a UTC date from API for display in local timezone */export function formatDateForDisplay(utcDate: string, format: string = 'DD/MM/YYYY HH:mm'): string { return dayjs.utc(utcDate).tz(DEFAULT_TIMEZONE).format(format);}
/** * Convert a local date input to ISO string for API */export function formatDateForAPI(localDate: string | Date): string { return dayjs.tz(localDate, DEFAULT_TIMEZONE).toISOString();}
/** * Build a date filter for API query */export function buildDateFilter( field: string, startDate: string, endDate: string, timezone?: string,): string { const params = new URLSearchParams(); params.set('filter', `${field}||$between||${startDate},${endDate}`); if (timezone) { params.set('timezone', timezone); } return params.toString();}React Example
Section titled “React Example”import { useState } from 'react';
interface DateRangeFilterProps { onFilter: (startDate: string, endDate: string) => void;}
export function DateRangeFilter({ onFilter }: DateRangeFilterProps) { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState('');
const handleSubmit = () => { // Dates sent as YYYY-MM-DD; the API handles timezone conversion onFilter(startDate, endDate); };
return ( <div> <input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} /> <input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} /> <button onClick={handleSubmit}>Filter</button> </div> );}
// Usage in a page componentfunction RecordsPage() { const fetchRecords = async (startDate: string, endDate: string) => { const response = await fetch( `/data-owner-bc/v1/records?filter=record_date||$between||${startDate},${endDate}`, ); const data = await response.json(); return data; };
return <DateRangeFilter onFilter={fetchRecords} />;}Vue.js Example
Section titled “Vue.js Example”<template> <div class="date-filter"> <input type="date" v-model="startDate" /> <input type="date" v-model="endDate" /> <select v-model="timezone"> <option value="UTC">UTC</option> <option value="Asia/Tokyo">Japan (GMT+9)</option> <option value="Asia/Singapore">Singapore (GMT+8)</option> <option value="America/New_York">New York (GMT-5)</option> </select> <button @click="applyFilter">Filter</button> </div></template>
<script setup lang="ts">import { ref } from 'vue';
const startDate = ref('');const endDate = ref('');const timezone = ref('UTC');
const emit = defineEmits<{ filter: [query: string];}>();
const applyFilter = () => { const params = new URLSearchParams(); if (startDate.value && endDate.value) { params.set('filter', `record_date||$between||${startDate.value},${endDate.value}`); } if (timezone.value !== 'UTC') { params.set('timezone', timezone.value); } emit('filter', params.toString());};</script>API Response Format
Section titled “API Response Format”All date/time values in API responses are returned in UTC ISO 8601 format:
{ "status": { "code": 200000, "message": "Request Succeeded" }, "data": { "type": "records", "id": "uuid-123", "attributes": { "record_date": "2024-01-15T07:30:00.000Z", "created_at": "2024-01-14T17:00:00.000Z", "updated_at": "2024-01-15T08:45:00.000Z" } }}Frontend applications are responsible for converting UTC to local display time using the user’s timezone preference.
Supported Timezones
Section titled “Supported Timezones”The system supports all standard IANA timezone identifiers:
| Region | Timezone | UTC Offset |
|---|---|---|
| Asia | Asia/Tokyo | +9:00 |
Asia/Singapore | +8:00 | |
Asia/Hong_Kong | +8:00 | |
Asia/Seoul | +9:00 | |
Asia/Shanghai | +8:00 | |
Asia/Bangkok | +7:00 | |
| Americas | America/New_York | -5:00/-4:00 (DST) |
America/Los_Angeles | -8:00/-7:00 (DST) | |
America/Chicago | -6:00/-5:00 (DST) | |
| Europe | Europe/London | +0:00/+1:00 (DST) |
Europe/Paris | +1:00/+2:00 (DST) | |
Europe/Berlin | +1:00/+2:00 (DST) | |
| Other | UTC | +0:00 |
Best Practices Checklist
Section titled “Best Practices Checklist”Application Setup
Section titled “Application Setup”- Set
process.env.TZto your deployment timezone in bootstrap - Configure
dayjs.tz.setDefault()to matchprocess.env.TZ - Extend dayjs with
utcandtimezoneplugins
Database Configuration
Section titled “Database Configuration”- Set
timezone: 'Z'in TypeORM connection config - Use
timestamptzcolumn type for all date/time columns - Store all dates in UTC format
API Design
Section titled “API Design”- Accept date-only format (YYYY-MM-DD) for user convenience in filters
- Support
timezonequery parameter for international users - Return all dates in UTC ISO 8601 format
- Document timezone behavior in API documentation
Frontend
Section titled “Frontend”- Convert UTC responses to local time for display
- Send dates in YYYY-MM-DD format for GET filters (let API handle conversion)
- Send complete ISO 8601 with timezone for POST/PUT/PATCH mutations
- Provide timezone selector for international applications
- Use dayjs consistently for all date operations
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”1. Dates are off by several hours
- Check that
timezone: 'Z'is set in TypeORM config - Verify
process.env.TZis set before any date operations
2. dayjs.tz is not a function
- Ensure plugins are extended before use:
dayjs.extend(utc);dayjs.extend(timezone);
3. Date range queries missing data
- Verify the query builder is expanding date-only values correctly
- Check that column type is
timestamptz, nottimestamp
4. Frontend shows wrong time
- Ensure UTC dates from API are converted to local time for display
- Check that dayjs timezone plugin is loaded in frontend
Summary
Section titled “Summary”This timezone management system ensures:
- Precision: Queries return data accurately based on the user’s local day/time
- Standardization: All database storage is in UTC for international compatibility
- Transparency: Developers write business logic using local time concepts; the system handles conversion automatically
The 3-layer approach (Application → Database → Query Builder) provides a robust foundation for timezone-aware operations in multi-region enterprise systems.