Days and time zones
View as MarkdownIdentity
Section titled “Identity”Workspace time zone storage, the server and browser accessors that read it, and the temporal codecs that project a stored instant onto a calendar.
Contract
Section titled “Contract”- Temporal is the time and date library. Parsing, arithmetic, comparison, and
formatting all go through it;
Date,dayjs, anddate-fnsare not used for any of the four.@sapporta/shared/temporalre-exportsTemporalfrom@js-temporal/polyfillbeside the codecs below, so one specifier carries both the runtime and the helpers built on it. - Timestamps are stored in UTC as fixed-width canonical text. A time zone decides which calendar day one of those instants falls on, and what wall clock it is read on.
- A day is a calendar day in the active workspace’s time zone. The
workspace row carries
timeZoneas an IANA identifier such asAsia/Kolkata. A fixed offset is not accepted: an offset describes one instant, a report describes a range, and a range can contain the moment the offset changes. - The zone is a business fact of the workspace. “Revenue for August 24” names one set of rows for every member of that workspace.
workspaceTimeZone(auth)from@sapporta/serveris the accessor a request handler calls. It reads the zone the request already resolved, so no route declares a time zone parameter and no handler performs a lookup.workspaceTimeZone()throws for a request with no workspace — an anonymous public route, or one holding onlysystemGlobalOnlyauthority. A request with no workspace has no calendar, and an error is the answer rather than UTC.appTimeZone()from@sapporta/frontend/platformis the accessor a React screen calls. It returns a plain value with no hook and nothing asynchronous: the zone is published once per page load from the auth-context response the boot sequence already fetches, and republished when a session switches workspaces.setDisplayTimeZoneanddisplayTimeZonefrom@sapporta/grid/column-presethold the value.@sapporta/gridneeds the zone to write a cell and does not import from the frontend, so there is one holder and a grid cell agrees with the screen around it. A date or timestamp column takes nozoneoption.TimeZonefrom@sapporta/shared/temporalis a checked identifier.parseTimeZone()checks an identifier a caller named and reports the bad value.isValidTimeZone()narrows, for a stored identifier that may have gone stale and whose answer is a fallback.supportedTimeZones()lists the identifiers a picker offers.deviceTimeZone()reports what the runtime says about itself, and is called only where that is the answer being asked for: the sign-up request, which carries the browser’s zone so that the first workspace an account creates starts on the calendar its owner keeps, andpnpm seed, which has no request to take a zone from and puts the seeded workspace on the clock of the machine that ran it.Temporal.Now.timeZoneId()andTemporal.Now.plainDateISO()with no argument read the host’sTZ. A framework test fails the build for any reader of an ambient zone other thandeviceTimeZone().- An owner changes the workspace zone on the workspace settings screen at
/workspace/settings, which callsPUT /api/auth-context/workspace/time-zoneand answers with a fresh auth context. Every member of the workspace reads the new calendar. - CSV export and grid clipboard copy emit the stored UTC instant with its
trailing
Z. A column a downstream program keys on stays self-describing.
Reading a stored instant
Section titled “Reading a stored instant”The codecs in @sapporta/shared/temporal take the zone as a required argument
and hold no state.
formatTemporalForDisplay(value, precision, zone)renders a canonical date or instant as reading text —2026-08-23and2026-08-23 16:38. Precision is a ceiling: a plain date asked for"minute"stays a date. A value in neither canonical shape is reported asnull, so a caller shows the text exactly as it arrived.formatInstantForDisplay(value, zone)anddescribeInstantForDisplay(value, zone)render the short and full forms. The full form names the offset —2026-08-24 02:00:00 (UTC+05:30)— so the zone a value was printed on is recoverable from the text.formatInstantForDateInput(value, zone)andparseDateInputToInstantString(day, bound, zone)move between a local calendar day and the instants that day occupies.boundis"startOfDay"or"endOfDay".localDayInZone(instant, zone)returns the calendar day an instant falls on.
Grouping and bounding by day
Section titled “Grouping and bounding by day”resolveDateRangeQueryBounds() from @sapporta/shared/daterange and the
to_tz_date() SQL function are the two day-shaped operations a handler
performs. Both are covered in
Group and filter by day.