Skip to content
TilloTech Docs

UTC datetime contract between Storefront and Hub

Status

accepted

Context

Storefront and Hub exchange datetime values across a backend/frontend boundary. Storefront's backend is required to operate in UTC, while Hub presents tenant-aware date and time controls.

The current Hub implementation does not consistently encode one timezone on the wire:

  • Order-history filters use formatDateToIsoStringWithTimezone, which includes the browser's local UTC offset (hub/apps/hub/app/utils/timezoneUtils.ts:84-108 and hub/apps/hub/app/layers/partner/storefront/store.ts:48-58).
  • Promotion creation sends YYYY-MM-DDTHH:mm:00 without a Z suffix or numeric offset (hub/apps/hub/app/layers/partner/promotions/utils/createStorefrontPromotionFormSchema.ts:126-134). The form validates against the tenant timezone, but does not encode that timezone in the payload.
  • Scheduled brand-rate changes likewise send a timezone-less datetime (hub/apps/hub/app/layers/partner/brands/components/brand-details/storefront-details/StorefrontRateForm.vue:177-192).
  • The Storefront proxy forwards requests without datetime normalization (hub/apps/hub/app/layers/partner/storefront/server/api/storefront/[...storefrontProxy].ts).
  • Ember's application timezone is UTC (config/app.php:172), and scheduled-change persistence converts tenant-local input to UTC before storing it (app/Helpers/DateHelper.php:23-29 and app/ScheduledChange/Service/ScheduledChangeService.php:35-70).
  • Backend response contracts are also mixed: scheduled changes are serialized as UTC with a Z suffix (app/Hub/Products/Http/Resources/ScheduledChangeResource.php:26-31), while promotions are serialized as tenant-local wall-clock values without an offset (app/Hub/Products/Promotions/Services/PromotionService.php:442-446 and app/Hub/Products/Promotions/Http/Resources/PromotionResource.php:21-29).
  • Backend validation treats promotion and scheduled-rate request values as tenant-local wall-clock values (app/Rules/AfterInTenantTimeRule.php:42-49 and app/ScheduledChange/Service/ScheduledChangeService.php:49-70), while order filters accept ISO values with an explicit offset (app/Hub/Orders/Http/Requests/OrdersRequest.php:17-24).

Consequently, a consumer cannot reliably determine whether a timezone-less value represents UTC, browser time, or tenant time.

Current implementation audit

Audited 12-08-2026 while wiring promotion conflict validation:

  • Promotion creation sends starts_at and ends_at as YYYY-MM-DDTHH:mm:ss, without an offset, from tenant-local date/time controls (hub/apps/hub/app/layers/partner/promotions/utils/createStorefrontPromotionFormSchema.ts:126-134).
  • Immediate standard-rate endpoints send no datetime.
  • Scheduled standard-rate creation and update send changing_at as YYYY-MM-DDTHH:mm:ss, without an offset, from tenant-local date/time controls (hub/apps/hub/app/layers/partner/brands/components/brand-details/storefront-details/StorefrontRateForm.vue:177-192, hub/apps/hub/app/layers/partner/brands/components/brand-details/storefront-details/ScheduledRateForm.vue:155-162).
  • Ember request validation currently accepts that exact timezone-less format for promotion creation and scheduled-rate create/update (app/Hub/Products/Promotions/Http/Requests/StorePromotionRequest.php:30-42, app/Hub/Products/Http/Requests/StoreProductScheduledChangeRequest.php:27-31, app/Hub/Products/Http/Requests/UpdateProductScheduledChangeRequest.php:37-43).
  • PromotionController::store interprets those promotion values in the tenant timezone before passing UTC instants to conflict validation. ScheduledChangeService likewise converts tenant-local scheduled values to UTC before persistence.
  • Current wire behaviour is therefore tenant-local wall-clock input, UTC internal processing/storage, and mixed response serialization. This is an implementation gap against the accepted UTC-on-wire decision, not a revision of that decision.
  • Scheduled-rate request validation originally compared changing_at against the server's now() value rather than an explicit tenant-timezone rule. Both scheduled-change requests now use AfterInTenantTimeRule, matching StorePromotionRequest, and the endpoint-specific timezone tests either side of UTC are in place (tests/Feature/Hub/Http/Controllers/Products/ProductScheduledChangeControllerTest.php).
  • The gap has a correctness cost, not only an ambiguity one. A tenant-local wall-clock value is not always a single instant: under DST it may not exist, or may occur twice. DateHelper::convertDateStringFromTenantTimeToUtc resolves both cases silently rather than rejecting them — a non-existent spring-forward time is shifted forward an hour, and the first of an ambiguous fall-back pair is chosen — so a scheduled rate change can be stored up to an hour away from what the user asked for, with no signal to them. A UTC instant on the wire removes this class of defect by construction. Current behaviour is characterised (not endorsed) in tests/Feature/Helpers/DateHelperTest.php, and EM-3961 covers closing the gap for the rate and promotion endpoints.

Decision Drivers

  • Storefront backend must always operate in UTC.
  • Ember's persisted scheduled-change instants are UTC, even where request values are tenant-local.
  • Hub and Storefront need an unambiguous backend/frontend contract for datetime values.
  • Hub currently uses tenant timezone for some validation and display flows, but that timezone is not consistently represented in submitted values.
  • The current proxy does not normalize datetime values.
  • Backend responses currently expose both UTC values and tenant-local wall-clock values.

Considered Options

UTC datetimes on the wire

Exchange absolute datetimes as ISO 8601 UTC values, using a Z suffix. Hub converts tenant-local user input to UTC before sending it and converts UTC responses to the tenant timezone for display.

Tenant-local datetimes on the wire

Exchange wall-clock values interpreted using each tenant's configured IANA timezone.

Mixed or implicit timezone handling

Allow each endpoint to choose between browser-local offsets, tenant-local wall-clock values, and UTC.

Decision Outcome

Adopt UTC as the datetime contract between Storefront and Hub.

Storefront backend datetime values are UTC. Datetimes exchanged between Storefront and Hub must be unambiguous ISO 8601 UTC values with a Z suffix. Hub remains responsible for converting between tenant-local values in the UI and UTC values at the API boundary.

Timezone-less datetime strings and browser-local offsets are not valid substitutes for this contract. Existing Hub flows that send either representation must be migrated to the UTC contract.

Consequences

Positive

  • Storefront and Hub have one unambiguous representation for exchanged datetimes.
  • Backend processing remains aligned with Storefront's UTC requirement.
  • Tenant timezone remains a presentation and input concern in Hub rather than being implicit in the API payload.

Negative

  • Hub's existing order-filter, promotion, and scheduled-rate payloads require changes.
  • Hub must perform explicit tenant-timezone-to-UTC conversion when submitting tenant-local form values.

Neutral

  • Existing tenant-timezone-aware validation and display behavior remains relevant; it must be paired with conversion at the request/response boundary.
  • Backend storage and runtime can remain UTC while API consumers receive or submit tenant-local values only at explicitly defined conversion boundaries.