The Last Screen Before a Customer Paid Was Showing the Wrong Day
Bakery Buddy's public invoice page has a confirmation gate before a customer can pay. It shows the pickup window, asks them to check a box confirming the day and time, and only then unlocks the payment button. It's one small screen, right before real money moves.
I was looking at that screen on a live order and asked a very literal question: is "16" right?
The number was right. The screen wasn't.
It was right. Marin's default pickup window in settings is stored as 10:00 to 16:00, twenty four hour time, because that's the format the database uses. The bug wasn't the data, it was that the page printed the raw setting straight onto a customer facing screen. Reading "10:00 to 16:00" means doing the math yourself to realize you can collect until 4pm. Bakery Buddy already had a function that turns that same value into "10:00 AM to 4:00 PM," and the pickup reminder emails had used it for weeks. This one page had just never called it.
On its own, that would have been a small, forgivable bug. The date sitting right next to it was worse, and it was wrong in two separate ways at once.
Wrong column, then wrong day
The gate showed the order's date as "event_date" if it existed, otherwise "fulfillment_date." Those look interchangeable and usually aren't. The event date is the day of the wedding or the birthday. The fulfillment date is the day the cake actually gets picked up, and bakers move it on purpose. A wedding cake collected the afternoon before the wedding is the normal case, not an edge case. This modal exists to confirm a pickup, so it should read the fulfillment date first. It read the event date first instead. On the order I was looking at, the event stayed on August 28 while the pickup had already moved to August 29. The gate was asking about the wrong day before a single line of date formatting even ran.
Then the formatting ran, and it made things worse. The code was "new Date(value).toLocaleDateString()" on a Postgres date column. A date only column like this one comes back from the database as a plain string, "2026-08-28," with no time and no timezone attached to it. "new Date()" parses that as UTC midnight, and "toLocaleDateString()" renders it back out in whatever timezone the browser happens to be in. In every US timezone, that's still the previous day. August 28 came out as 8/27.
Stack both bugs and the modal was asking a customer to confirm August 27, for a pickup that was actually scheduled for August 29. Two days off, on the last screen before they paid, with the correct data sitting right underneath the wrong display and nothing anywhere that looked like an error.
Why this one mattered more than a normal display bug
A wrong date on a page nobody acts on is cosmetic. This one sat directly in front of a decision. A customer either checks the box and pays, or stops to double check with the bakery, or worst of all just shows up on the day the screen told them. None of those are good, and the middle one, a customer emailing to ask whether it's actually the 27th or the 29th, is exactly the back and forth this page exists to prevent.
The fix
Two changes, matched to the two bugs. The pickup window now goes through the same time formatting helper the reminder emails already use, so it reads "10:00 AM to 4:00 PM" instead of the raw setting. And the date the gate shows now prefers "fulfillment_date" over "event_date," formatted with a helper that parses and renders a date only column pinned to UTC instead of the viewer's local zone, so a Postgres date reads back as the same calendar day it was stored as, no matter where the browser sits.
That second part wasn't a new idea in this codebase. The exact trap, calling "new Date()" on a date only column, had already been written up as a rule after an earlier, unrelated bug in a completely different part of the app. It happened again anyway, in a file nobody had connected to that earlier lesson. Writing a rule down tells the next person what went wrong once. It does nothing to stop a third person from typing "new Date(value)" in a new file, because nothing actually enforces it.
Verifying it meant standing in the customer's timezone, not building the app
"npm run build" passes on both the broken version and the fixed one. So does the type checker. The bug is invisible from the servers this app actually runs on, because those run in UTC, and "2026-08-28" parsed and re-rendered in UTC gives back August 28, the correct day. The bug only shows up once you render in a timezone behind UTC, which is every timezone a customer in the United States is actually standing in.
So I checked it that way. Not by reading the code and reasoning about what it should do, but by rendering both the before and after against the real order under a Pacific timezone:
- Before: 8/27/2026, and 10:00 to 16:00
- After: Saturday, August 29, and 10:00 AM to 4:00 PM
That's the only check that would have caught this before a customer did.
What generalizes past dates
Two lessons here, and only one of them is about calendars.
The specific one: if a column in your database is a date with no time attached, "new Date(value)" is not a safe way to read it back. It silently assumes UTC, and rendering that in a local timezone hands you the wrong day in exactly the timezones most of your users probably live in. Parse and render date only values pinned to UTC, on purpose, through one shared function, not a one off "new Date()" wherever a date needs to show up on a screen.
The bigger one: a rule written into a docs file is not a rule the code actually follows. This exact mistake had already been made, found, fixed, and documented once before it happened again somewhere else. Documentation doesn't stop the third occurrence. The only thing that does is making the correct path the only path, one formatting function that every date column has to go through, so there's nothing left to accidentally get wrong the next time.
Frequently Asked Questions
What was actually wrong with the pickup confirmation screen?
Two separate bugs stacked together. The screen showed the order's event date instead of its fulfillment date (the actual pickup day, which can legitimately differ), and then rendered that date with new Date().toLocaleDateString(), which turns a date-only database value into the previous calendar day in every US timezone. Combined, a customer was shown a pickup date two days off from the real one.
Did a real customer see the wrong date?
This was live on the actual page customers use to confirm and pay, not a preview or a staging environment. I can't say how many people saw the wrong day before it was caught and fixed, only that the page was there to see it on.
What's the actual fix?
The gate now prefers the order's fulfillment date over its event date, and formats it with a helper that parses and renders date-only values pinned to UTC instead of the browser's local timezone. The pickup window also now goes through the same 12-hour time formatter the reminder emails already used, instead of printing the raw 24-hour setting.
Why does this matter if you're not building a bakery app?
Any app with a date-only database column (no time, no timezone) has this exact trap available to it: new Date(value) silently assumes UTC, and rendering it locally can hand back the wrong day. And the bigger lesson generalizes even further: writing a rule down after you first find a bug doesn't stop it from happening again in a different file. Only making the correct function the only available path does that.
I build Bakery Buddy for my wife Lindsay's cake studio, Marin Cake Studio, and I check every date this app renders against the timezone a real customer is actually standing in.
Ready to put this into practice?
Join the Waitlist