A report API filtering by date range. Server parses the date as UTC midnight. Users in IST see reports missing the first 5.5 hours of every day. Indian product → almost every customer hits the bug.
The vulnerable diff
// api/reports.ts
reportsRouter.get("/reports", async (req, res) => {
// BUG: UTC midnight, not user's local midnight
const start = new Date(req.query.date + "T00:00:00Z");
const end = new Date(start.getTime() + 24 * 60 * 60 * 1000);
const events = await db.events.findMany({
where: { createdAt: { gte: start, lt: end } },
});
res.json(events);
});What is wrong
Date strings without timezone are ambiguous. `new Date('2026-09-15T00:00:00Z')` is UTC midnight. For a user in IST (UTC+5:30), IST midnight is `2026-09-14T18:30:00Z` UTC. A 'date' query that uses UTC interprets the day boundary 5.5 hours later than the user intended — events from 00:00-05:29 IST are excluded from the day's report. The fix is to interpret the date string in the user's timezone using a library like `date-fns-tz` or `Temporal`.
The attack
Symptom:
User in Bangalore filters for 2026-09-15.
Expected: events from 2026-09-15 00:00 IST → 24:00 IST.
Actual: events from 2026-09-15 00:00 UTC → 24:00 UTC.
Skipped: 5.5 hours of events at the start of the day.
Reports look wrong, sometimes empty for early-morning bookings.Indian e-commerce, fintech, and SaaS see this constantly — IST is far from UTC.
Mesrai's review comment
mesraipilot · Bot · reviewed 1 min ago
[mesrai] [code-review] [Logic] [Timezone] [medium]
Date parsed as UTC midnight. For non-UTC users (Asia/Kolkata is +5:30
ahead), this skips the first hours of their local day.
Interpret the date in the user's timezone:
import { fromZonedTime } from "date-fns-tz";
const userTz = req.user.timezone ?? "Asia/Kolkata";
const start = fromZonedTime(req.query.date + "T00:00:00", userTz);
const end = fromZonedTime(req.query.date + "T23:59:59.999", userTz);
Or use Temporal (now standard in modern Node):
const day = Temporal.PlainDate.from(req.query.date);
const start = day.toZonedDateTime(userTz).toInstant();
const end = day.add({ days: 1 }).toZonedDateTime(userTz).toInstant();
Persist the user's timezone (account settings, infer from browser
on signup). Default to a sensible region default when missing.The fix
// api/reports.ts — fixed
import { fromZonedTime } from "date-fns-tz";
reportsRouter.get("/reports", async (req, res) => {
const userTz = (req.user.timezone as string) ?? "Asia/Kolkata";
const start = fromZonedTime(req.query.date + "T00:00:00", userTz);
const end = fromZonedTime(req.query.date + "T00:00:00",
userTz).valueOf() + 24*60*60*1000;
const events = await db.events.findMany({
where: { createdAt: { gte: start, lt: new Date(end) } },
});
res.json(events);
});Convert the user-supplied date string from the user's timezone to absolute Instant. The query now matches the day the user thinks of.
Why human review missed it
Timezone bugs are silent because dev environments often run in IST while servers run in UTC — the discrepancy doesn't show until the report doesn't match. Mesrai catches every date-from-string parse that doesn't pass an explicit timezone.
Related rules + further reading
Mesrai rule pack: logic/date-parse-timezone — flags date string parsing without explicit timezone in user-facing filters.
MDN: Date — Date string formats.
Very common in Indian SaaS due to the 5.5-hour UTC offset.
Takeaway
Date strings + UTC = bug. Convert from user TZ at the boundary. Mesrai catches every untimezoned parse.