TLDR: Every layer of a modern web stack — Postgres on Supabase, Node on Vercel, the Zoom API, JS date parsing — defaults to UTC. If your business runs on Eastern Time, you have to opt in to ET explicitly, at every boundary. Here's exactly how.

The Setup

I build internal tooling for an ecommerce business (a supplement company that ships products, runs webinars, and manages its ops entirely on Eastern Time).

Last month I was deep in two apps at once: an internal supply-chain app (Shopify order sync, FIFO inventory depletion) and an internal KOL CRM (KOL relationship management with a Zoom webinar board).

Both had date bugs. Same root cause.

The Wall We Hit

A 9pm ET order was appearing on the next day's supply chart.

And on the KOL board, Zoom webinars were auto-matching to the wrong calendar date.

Both felt like display bugs. They weren't.

What Was Actually Wrong

When Postgres casts a timestamptz to date for bucketing, it uses the server's timezone — which on Supabase is UTC. A 9pm ET order is 01:00Z the next day, so it falls into tomorrow's bucket.

-- ❌ this buckets in UTC
fulfilled_at::date

-- ✅ this buckets in ET
(fulfilled_at AT TIME ZONE 'America/New_York')::date

The Zoom problem was the same shape. Zoom returns start_time in UTC. An 8pm ET webinar's UTC timestamp is the next calendar day. Without converting to ET first, the matcher silently buckets it wrong and the webinar just… doesn't match.

Fix: Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York' }) on the Zoom timestamp before comparing against the scheduled date.

The Trap That Almost Got Me (A Different App)

My reflex on date display bugs is always: "just slap a timeZone on the formatter."

There's a case — different app, different bug — where that would've been catastrophic.

In a webinar project-management app, event times were showing UTC in an audit log. My first instinct was to add timeZone: 'America/New_York' to the toLocaleString call. I checked the stored value first.

Turns out the stored timestamp was a naive wall-clock string labelled UTCnew Date("2026-06-05T08:09").toISOString() running on a UTC server gives you 08:09Z. The display was already showing exactly what the user typed. Adding the timeZone correction would've shifted everything 4 hours earlier and broken the editor's agreement with the list view.

The lesson: before you "fix" how a date displays, know whether the stored value is a true instant (UTC-anchored, TZ-correct to convert) or a wall-clock label (whatever the user typed, stored naively).

The Pattern That Actually Works

Three specific rules I apply now across every ET-anchored app:

  • SQL bucketing: always AT TIME ZONE 'America/New_York' on timestamptz-to-date casts. Not on time-arithmetic comparisons — those are TZ-safe already.
  • JS date-only strings: new Date('2026-05-19') parses as UTC midnight and displays as May 18 in ET. Fix: append T00:00:00 to force local-timezone parse. Encapsulate it once in a parseLocalDay() helper.
  • Server Components on Vercel: toLocaleString() runs in UTC, not the browser's TZ. Pass { timeZone: 'America/New_York' } explicitly — every single call.

What you don't touch: timestamptz columns (TZ-aware natively), cron schedules in vercel.json (UTC is fine for sync jobs), rolling-window comparisons (>= p_as_of - interval '30 days').

Why This Matters to Me

It's the kind of bug that shows up in the data, not the logs. A chart that's off by a day, a webinar that won't match. Infuriating to debug if you don't know where to look.

Now every new app gets the timezone question answered on day one: what timezone does this business operate in, and have I made every layer opt in to that explicitly?

The answer for an ecommerce business is always ET. The stack's default is always UTC.

One of those has to win. Make sure it's the right one.