Date and time handling¶
Overview¶
All date and time formatting across the BMS, client portal, and Edge Functions uses centralised utility functions that format in the configured timezone — Australia/Sydney by default.
Before this was set up, dates were formatted inconsistently. Some used the browser's local timezone, some used UTC, and several Edge Functions extracted dates using .toISOString().split('T')[0], which uses UTC. The most common bug was dates shifting by a day: a timestamp at 11pm UTC on April 16 is already April 17 in Sydney, so any UTC-based date extraction would show the wrong day for about 10 hours every night.
The timezone lockdown project (completed April 2026) standardised all date formatting through three utility files — one per runtime — and added governance to prevent anyone from bypassing them.
How to format dates¶
BMS (React)¶
import { useDateFormatter } from '@/hooks/useDateFormatter';
function MyComponent({ record }) {
const { formatDate, formatDateTime, formatShortDate, formatTime } = useDateFormatter();
return (
<div>
<span>{formatDate(record.created_at)}</span> {/* "17/04/2026" */}
<span>{formatShortDate(record.created_at)}</span> {/* "17 Apr 2026" */}
<span>{formatDateTime(record.created_at)}</span> {/* "17 Apr 2026, 2:30 pm" */}
<span>{formatTime(record.created_at)}</span> {/* "2:30 pm" */}
</div>
);
}
The useDateFormatter hook reads the timezone from organisation_settings (cached by TanStack Query) and returns pre-bound format functions. You never need to pass the timezone yourself.
Other available functions: formatLongDate ("17 April 2026"), formatLongDateTime ("17 April 2026 at 2:30 PM" — uppercase AM/PM, for emails), toDateString ("2026-04-17" — timezone-aware YYYY-MM-DD).
Client Portal (React)¶
import { useDateFormatter } from '../hooks/useDateFormatter';
// Same API as BMS — same function names, same output formats.
The portal's useDateFormatter reads the timezone from the portal-org Edge Function response (already fetched by Layout.tsx on every page load), so there's no extra network call.
Edge Functions (Deno)¶
import { getTimezone, formatDate, formatDateTime, formatShortDate } from '../_shared/utils.ts';
const tz = await getTimezone(); // reads from organisation_settings, cached per instance
const formatted = formatDate(record.created_at, tz);
Edge Functions don't have hooks, so you call getTimezone() yourself and pass the result to each format function. getTimezone() accepts an optional Supabase client (getTimezone(supabase)) but also works without one — it falls back to a raw PostgREST fetch.
What NOT to do¶
These patterns are banned across the entire codebase. Do not use them outside the three utility files.
| Banned pattern | Why it's banned |
|---|---|
toLocaleDateString() |
Uses the browser's local timezone, not Sydney. A user in Perth or London sees a different date. |
toLocaleTimeString() |
Same timezone issue. |
new Intl.DateTimeFormat() |
Bypasses the centralised utilities. If the format needs to change later, this call site would be missed. |
.toISOString().split('T')[0] |
Extracts the UTC date, not the Sydney date. Near midnight, this returns yesterday's date in Sydney. |
.toLocaleString() for dates |
Same timezone issue. (.toLocaleString() is fine for currency — the ban is about using it for date formatting.) |
What happens if you break the rules¶
BMS: ESLint no-restricted-syntax rules will fail your build immediately. The error message tells you which pattern is banned and points to src/lib/dateFormat.ts as the correct alternative.
Client Portal and Edge Functions: No ESLint enforcement yet. Two other mechanisms catch violations:
- The weekly AI review (runs Monday 4am UTC) scans all source files for banned patterns and flags them in a GitHub Issue.
- Claude Code is instructed via CLAUDE.md to always use the utility files. If CC is writing your code, it will follow this rule automatically.
Adding a new date format¶
If you need a format that doesn't exist (e.g. a weekday name, a month-year without day):
- Add the function to all three utility files — they must stay in sync:
apps/bms/src/lib/dateFormat.tsapps/client-portal/src/utils/dateFormat.tssupabase/functions/_shared/utils.ts- Add the hook wrapper in both apps:
apps/bms/src/hooks/useDateFormatter.tsapps/client-portal/src/hooks/useDateFormatter.ts- Add tests in all three test files (see below)
- Update the reference doc:
docs/live/date-time-handling.md
Do not add one-off formatting in a component or page file. The whole point of the system is that all formatting goes through the utility files.
Where the tests are¶
| File | What it tests |
|---|---|
apps/bms/src/lib/dateFormat.test.ts |
BMS utility — all 7 functions |
apps/client-portal/src/utils/dateFormat.test.ts |
Portal utility — same coverage |
supabase/functions/_shared/dateFormat.test.ts |
Edge Function utility — same coverage |
supabase/functions/portal-ticket-detail/dateFormatting.integration.test.ts |
Calls the real Edge Function on dev to verify the full pipeline |
Run unit tests: npm run test (from repo root — runs all three suites)
Run Edge Function integration tests: npm run test:integration:edge (requires .env.test with dev Supabase credentials)
Further reading¶
docs/live/date-time-handling.md— full technical reference. Claude Code reads this before modifying any date/time code.CLAUDE.md— banned patterns and utility sync rules that CC follows automatically.