Skip to main content
Full narrative: Event clock writeup (ENG-580). Inventory SQL and leftover queries live on Event clock cohort alignment. Stored-active vs display-active by writer interval: Event clock active intervals. Availability writers and engagement accept: Availability and engagement clock semantics. Ledger calendar-date leftovers are ENG-581 / PR #1370, not this canvas. Storage target is real UTC Instants on call clocks. Migrations and this-pass fixes are branch-only — not deployed. Instant writers are not in production.
Prod counts below are the 2026-08-17 snapshot. DBeaver on a Central machine uses session America/Chicago. Naive TIMESTAMP ::date / ::time are safe. (timestamptz)::date is not.

The split

Docs and Prisma comments said call clocks were UTC Instants, rendered in Event.timezone / Organization.timezone (Null = UTC). Runtime stored the opposite for almost every product writer: venue wall-clock digits in a naive TIMESTAMP(3) with a fake Z (17:00 → 2026-08-05 17:00:00). Will’s SQL rule (naive timestamps are UTC Instants; product conversion belongs in the app) is still the default. Call-time runtime is the exception. Treating 17:00Z as an Instant and projecting it through Chicago is the Mapache email bug (12:00–6:00 PM vs crew 5:00–11:00 PM). Event.timezone (or org) null does not mean the digits are Instants. It means no IANA label is stored. Readers then assume UTC. Flex create without metadata.timezone writes 'UTC' — the same lie, not Instant proof. Organization.timezone has no API writer. 17180000 fills it when Events collapse to one real IANA. Mixed orgs stay null unless already written in prod. Do not treat a still-null org TZ as Instant proof.

Target after alignment

Two corrections, never mixed:

Writer timeline

created_at names a writer era only if the row was never saved again. A later header save re-encodes the clocks. Empty windows in the 2026-08-17 snapshot (o_early_z, a_early_z, then-empty post-#1328 Flex) were no-ops. Later #1328 / #1387 writes fill e_after_flex_wallclock with Formula A walls — restore from the quote Instant.

What the migration already rewrites

20260817180000_event_clock_alignment — prove, then rewrite. Never convert a whole window. Sibling IANA is a minority path: 12 orgs / 314 events single_iana, 165 orgs / 596 events have no IANA, 1 org is Chicago+Detroit.

Leftovers that are classified

No more inventory needed. Do not convert. Already Instant — leave (do not Formula-B): Flex startTime = show_start_at (10); avail device-offset 5→4 (15 — migration nulls as all-day, not wall→Instant); respondedAt. 20260817180000_event_clock_alignment owns the cutover (no six-file split). Hour-0 availability prefixes and after-cutoff evenings Formula B in this file (upper bound < 2026-09-15). Device Instant evenings leave. Remaining unlabeled Events after fill have no startTime. RoS has 0 clocks. Require Event.timezone later; this file does not SET NOT NULL. Phase 4/5 branch code (not prod): notifications, documents, duplicate/RoS encode, inbox, Google/offer topology (one resolved zone on create + resync), Staffing Board Event-IANA encodes, mobile schedule past_midnight + per-row civil dates, portal/storefront resolved timezone on the API contract, engagement event_timezone, timeline civil-day, and many client readers — deploy with the migrations. 20260819170000_event_schedule_item_formula_b writes schedule-item Formula B SQL (hour 10–23, inverted overnight ends including exact 00:00:00, invalid Event IANA → org, five dump ids skip-as-Instant; ceiling < 2026-09-15) — not applied in prod, and no rows convert while convert_schedule_wall is empty. This pass (branch only): MOBILE-01, NOTIFY-01, CAL-03, CLIENT-01, CAL-02, WEB-02, MOBILE-02, CONTRACT-01. See merge / deploy. Fresh dump prod-postgres-20260819-pm.dump leftovers (classify only — not converted this pass): 145 Event evening starts with no IANA; 874 schedule evenings before Jul 15 floor; 65 in-window schedule evenings with no Event/org IANA; 686 Events with neither Event nor org IANA; MIG-02/03/04 empty (0 rows each). Classifier convert sets: 35 Event evening (not Flex-equal); 109 schedule evening (not Event-equal, not skip-listed); 5 schedule skip-as-Instant; 7 other low-hour schedule starts unconverted.
  • #379 mobile Events (0 starts).
  • Organization.timezone, ledger dates.

Still open

Prove wall-clock (or already-Instant) before picking a convert zone.

Zone ladder

  1. Event.timezone if it is a real IANA (not UTC).
  2. flex_quotes.metadata->>'timezone' (skip UTC).
  3. tz-lookup on Event lat/lng, then Venue via Event.venue_id (skip 0,0). Same helper web uses on save. api-events has no server lookup; a one-off pass can run outside Postgres.
  4. Sibling Event IANA (mode per org) — minority.
  5. Organization.timezone — required. Single-IANA fill in 17180000; mixed / leftover-Event orgs stamped in prod 2026-08-17 and 2026-08-18; leftover empty orgs Chicago-filled then SET NOT NULL. Product writer exists (create defaults UTC; settings update). ENG-588 replaces Chicago placeholders with Clerk / PostHog where known.
  6. Venue.address.state (IL / Illinois, …).
  7. Default America/Chicago — label fill only, after a sample of venues agrees.
Venue has no timezone column. FlexQuote.venue_id is a bare string, not a Prisma FK. The one multi-IANA org (Chicago + Detroit) is why venue coords beat an org-level CT default. Query 19 is the human check; summarize, do not paste the grid.

How to read the leftover queries

Paste-safe counts: queries 1–17 on the alignment page. Human look, do not paste names / cities / org ids — send a one-liner:

Out of scope

  • ENG-581 / PR #1370 — LedgerItem.date calendar day, payable snap, 20260307 leftovers.
  • API / client writers and readers — Phase 1 Event save passes Instants through; Phase 4/5 branch code (not prod) formats/rebases call clocks through Event IANA (Staffing encodes, mobile schedule civil-day + past_midnight, portal/storefront timezone on the API contract, one Google zone on create/resync, projection shift-by-gap). This pass (branch only): cancellation civil dates (NOTIFY-01), accepted ICS duration (CAL-03), RoS second-save reseed (CLIENT-01), resolved Event DTO timezone (CAL-02), mobile civil-day grouping (MOBILE-01), CRM/DayStripPreview readers (WEB-02/MOBILE-02), event_timezone type (CONTRACT-01). Deploy migrations + reader cutover together. Import/CRM and HubSpot-Flex n8n inbound paths stay a later pass.
  • Rewriting applied migrations — never edit 20260307. This work is a new file, 20260817180000_event_clock_alignment.