The Backend Timezone Audit: 14 Hidden Date Bugs
A walk appeared on the wrong calendar date. The audit that followed found 14 places extracting dates from UTC timestamps without timezone conversion.
A user's 7:07 PM walk on December 27th appeared on December 28th in the planner. The walk timestamp was stored in UTC (2025-12-28T00:07:00Z), and the API extracted the date without converting to the user's timezone. I audited the entire codebase and found 14 locations with the same bug.
The Patterns
Pattern 1: Carbon::today() without timezone. Controllers using Carbon::today() got the server's UTC date, not the user's local date:
// BEFORE: server timezone (UTC)
$today = Carbon::today();
// AFTER: user's timezone
$today = Carbon::today($user->timezone);
Pattern 2: The whereDate() trap. Laravel's whereDate() extracts the date from a UTC timestamp column, which can return wrong results for users behind UTC:
// BEFORE: compares UTC date portion -- wrong for EST users after 7pm
$query->whereDate('created_at', $localDate);
// AFTER: compare within the user's full local day range
$startOfDay = Carbon::parse($localDate, $user->timezone)->startOfDay()->utc();
$endOfDay = Carbon::parse($localDate, $user->timezone)->endOfDay()->utc();
$query->whereBetween('created_at', [$startOfDay, $endOfDay]);
Pattern 3: Console commands with no user context. Scheduled commands like GenerateRecurringTasks used server time. Fix: iterate over users, convert to each user's timezone.
The Count
| Location | Category |
|---|---|
| 5 API controllers | Carbon::today() without timezone |
| 3 console commands | Server timezone assumed |
| 3 query builders | whereDate() on UTC columns |
| 3 mobile utilities | toISOString().split('T')[0] (covered in a previous post) |
The fix for each is the same principle: always convert UTC to the user's timezone before extracting a date. Search your codebase for Carbon::today() and whereDate() -- you might be surprised.
Built while working on PlanPal.
Back to danielships ->