Promoted to Evolith: This ADR has been elevated to Evolith ADR-0072 — UTC Date Storage, Browser Timezone Detection, and Language Resolution. UMS retains this document as implementation reference.
Status: Accepted
Date: 2026-06-02
Context: Evolith Platform — Root Standard (applies to all child systems: UMS, and future products)
Supersedes: None
Related: ADR-0052 (Immutable Audit Trail), ADR-0056 (Clean Architecture Frontend)
Distributed systems that span multiple countries and time zones must handle dates, times, and locale preferences consistently across all layers. Without an explicit standard, individual teams make incompatible choices: some store local times, some use browser-default formatting, some hardcode language identifiers. When data crosses system boundaries or is shown to users in different time zones, these inconsistencies cause display errors, audit discrepancies, and compliance failures.
Three independent concerns must be addressed together because they interact at session initialization:
All Evolith child systems (current: UMS; future: billing, logistics, port operations, ERP integrations) must follow the same standard so that cross-system audit trails, delegations, and events are interpretable without time-zone ambiguity.
Rule: Every date or timestamp stored in any database table, domain event, outbox message, or API response body must be UTC.
DateTime.UtcNow or DateTimeOffset.UtcNow. Never use DateTime.Now.UtcNow at the domain level and Utc at the persistence level (e.g., CreatedAtUtc, DeletedAtUtc).ValueConverter<DateTime, DateTime> on all DateTime properties that forces DateTimeKind.Utc on read, preventing silent local-time storage on SQLite (which stores no timezone information).Z suffix (e.g., "2026-06-02T15:30:00Z") to make UTC explicit to consumers.new Date(isoString) — JavaScript always interprets Z-suffixed strings as UTC internally. Never manually apply offsets to UTC values before storing.Rationale: UTC is the only unambiguous anchor for distributed systems. Local times introduce DST gaps, wall-clock ambiguity, and cross-datacenter inconsistency. Storing UTC and converting at display time is the industry standard (IANA, RFC 3339, ISO 8601).
Rule: On login, the frontend detects the browser’s IANA timezone using Intl.DateTimeFormat().resolvedOptions().timeZone and stores it in the authenticated session state. It is sent to the backend as the X-Timezone request header on every API call.
Intl.DateTimeFormat().resolvedOptions().timeZone returns an IANA identifier (e.g., "America/Lima", "Europe/Madrid"). This is supported in all modern browsers.UI_TIMEZONE_DEFAULT (configured by tenant admin, e.g., "America/Lima")"UTC" (last resort only)undefined.X-Timezone header is validated against the IANA timezone database and stored in the request context. It is used for any server-side date formatting (e.g., report generation, email timestamps).Intl.DateTimeFormat with an explicit timeZone option.Rationale: The browser knows the user’s actual timezone without requiring manual configuration. Defaulting to the tenant timezone handles edge cases (kiosk terminals, SSO sessions from foreign browsers) while still providing a sensible default for most users in the same region as the tenant.
Rule: The UI language is resolved at session initialization in this order:
| Priority | Source | Mechanism |
|---|---|---|
| 1 (highest) | Browser Accept-Language HTTP header |
Read by backend CultureMiddleware, validated against supported locales |
| 2 | Tenant parameter UI_LANGUAGE_DEFAULT |
Read from in-memory configuration cache at login |
| 3 (lowest) | Platform hardcoded default "es" |
Final fallback |
CultureMiddleware reads Accept-Language, extracts the primary language code (first 2 characters, lowercased: "es-PE" → "es"), validates it against the supported locale list, and sets CultureInfo.CurrentCulture for the request.LoginSuccessResponse.Language AND in SessionParameters.DefaultLanguage.sessionParameters.defaultLanguage from the response and calls useI18nStore.setLanguage(lang) to initialize the active UI language for the entire session.["es", "en"].formatDate, formatDateTime, and formatRelativeTime calls must receive the active locale from the i18n store. Functions must not use a hardcoded default locale.Rationale: Browser preference reflects what the user has configured at the OS level. Respecting it requires no manual configuration and covers the majority of cases. The tenant admin can control the fallback for environments where browser settings are not representative (shared kiosks, managed devices).
Intl API (available in all modern browsers; not a real constraint).X-Timezone header adds a small overhead to every request (single string, negligible).DateTime properties in domain entities use DateTime.UtcNow.ValueConverter for DateTime properties.Z suffix.CultureMiddleware reads Accept-Language and validates against supported locales.Language (resolved) and SessionParameters.DefaultTimezone.Intl.DateTimeFormat().resolvedOptions().timeZone at login.X-Timezone header.sessionParameters.defaultLanguage at login.formatDate/formatDateTime calls pass the active locale and session timezone.