| name | timezone-seeding-day-of-week-bug |
| description | Fix validation errors "Start time must be before end time" when seeding time-based
data with timezone conversion. Use when: (1) Database seeding fails with time ordering
validation errors despite correct local times, (2) Timezone conversion causes day-of-week
changes that break time comparisons, (3) Early morning times (00:00-06:00) wrap to
previous day after UTC conversion, (4) Seeding availability blocks, schedules, or
recurring time-based data. Covers proper timezone conversion with day tracking and
exclusive date ranges.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-05T00:00:00.000Z" |
Timezone Seeding Day-of-Week Bug
Problem
When seeding time-based database records with timezone conversion (e.g., availability
blocks, schedules, recurring events), validation errors like "Start time must be before
end time" occur even though the local times are correct. The root cause is that timezone
conversion can change the day-of-week, making the end time appear to come before the
start time when stored in UTC.
Context / Trigger Conditions
Symptoms:
- Database seeding fails with validation error: "Start time must be before end time"
- Validation error: "Effective from must be before effective until"
- Local times are correct (e.g., 9:00 AM - 5:00 PM) but fail when converted to UTC
- Issue occurs primarily with early morning times (midnight to ~6 AM)
- Error appears in timezones with negative UTC offsets (e.g., America/Los_Angeles UTC-8)
When this occurs:
- Seeding availability blocks, schedules, or recurring events
- Converting local time to UTC for database storage
- Using date-fns-tz or similar timezone conversion libraries
- Time validation happens after conversion but before storage
- Day-of-week tracking is needed (e.g., "Monday 9:00 AM - 5:00 PM")
Root Cause
Timezone conversion can shift times across day boundaries, changing the day-of-week:
Example (London timezone, UTC+0):
fromZonedTime('2024-01-01T00:00:00', 'Europe/London')
fromZonedTime('2024-01-01T17:00:00', 'Europe/London')
Example (Los Angeles timezone, UTC-8):
fromZonedTime('2024-01-01T00:00:00', 'America/Los_Angeles')
fromZonedTime('2024-01-01T01:00:00', 'America/Los_Angeles')
The Problem:
const startTime = fromZonedTime('2024-01-01T00:00:00', 'America/Los_Angeles');
const endTime = fromZonedTime('2024-01-01T17:00:00', 'America/Los_Angeles');
startTime.toISOString()
endTime.toISOString()
What happens:
- Local Monday 5:00 PM (17:00) converts to Tuesday 1:00 AM UTC
- Time portion extracted: "01:00:00Z"
- Day-of-week changes from Monday (1) to Tuesday (2)
- When comparing just times: 01:00 < 08:00, validation fails
- Database schema expects times on SAME day-of-week
Solution
Fix 1: Track Day-of-Week During Conversion
Create a helper function that returns BOTH the converted time AND the day-of-week:
import { fromZonedTime } from "date-fns-tz";
import { format } from "date-fns";
function availabilityTimeToUTC(
dayOfWeek: number,
time: string,
timezone: string,
): { dayOfWeek: number; utcTime: Date } {
const referenceDate = new Date("2024-01-01");
referenceDate.setDate(referenceDate.getDate() + dayOfWeek);
const [hours, minutes] = time.split(":").map(Number);
referenceDate.setHours(hours, minutes, 0, 0);
const utcDate = fromZonedTime(referenceDate, timezone);
return {
dayOfWeek: utcDate.getUTCDay(),
utcTime: new Date(`1970-01-01T${format(utcDate, "HH:mm")}:00Z`),
};
}
Usage in seeding:
await db.availability.create({
data: {
userId: user.id,
dayOfWeek: 1,
startTime: fromZonedTime('2024-01-01T00:00:00', timezone),
endTime: fromZonedTime('2024-01-01T17:00:00', timezone),
timezone,
},
});
const start = availabilityTimeToUTC(1, "00:00", timezone);
const end = availabilityTimeToUTC(1, "17:00", timezone);
await db.availability.create({
data: {
userId: user.id,
dayOfWeek: start.dayOfWeek,
startTime: start.utcTime,
endTime: end.utcTime,
timezone,
},
});
Fix 2: Use Exclusive End Dates for Date Ranges
For single-day events or blockouts, use exclusive end dates (next day) to avoid
validation errors:
await db.availability.create({
data: {
userId: user.id,
status: "BUSY",
effectiveFrom: new Date("2024-12-25"),
effectiveUntil: new Date("2024-12-25"),
},
});
const blockoutDate = new Date("2024-12-25");
const exclusiveEnd = new Date(blockoutDate);
exclusiveEnd.setDate(exclusiveEnd.getDate() + 1);
await db.availability.create({
data: {
userId: user.id,
status: "BUSY",
effectiveFrom: blockoutDate,
effectiveUntil: exclusiveEnd,
},
});
Verification
-
Test with Multiple Timezones:
TIMEZONE=Europe/London pnpm db:seed
TIMEZONE=America/Los_Angeles pnpm db:seed
TIMEZONE=Asia/Hong_Kong pnpm db:seed
-
Verify Seeded Data:
const blocks = await db.availability.findMany();
for (const block of blocks) {
const startDay = block.startTime.getUTCDay();
const endDay = block.endTime.getUTCDay();
console.log(`Block ${block.id}:`);
console.log(` Stored dayOfWeek: ${block.dayOfWeek}`);
console.log(` Start time day: ${startDay}`);
console.log(` End time day: ${endDay}`);
console.log(` Match: ${block.dayOfWeek === startDay && startDay === endDay}`);
}
-
Check Validation:
const invalidBlocks = await db.availability.findMany({
where: {
startTime: { gte: db.raw('end_time') },
},
});
console.log(`Invalid blocks: ${invalidBlocks.length}`);
Example
Real-world bug from RoadDux driving instructor platform seeding:
Symptom: Seeding failed with validation error for Emily Chen's availability.
Error:
ORMError: Invalid createMany args for model 'Availability':
startTime: Start time must be before end time
Root Cause:
await db.availability.createMany({
data: [
{
userId: emilyId,
dayOfWeek: 1,
startTime: fromZonedTime(
new Date("2024-01-01T00:00:00"),
"America/Los_Angeles"
),
endTime: fromZonedTime(
new Date("2024-01-01T17:00:00"),
"America/Los_Angeles"
),
timezone: "America/Los_Angeles",
},
],
});
Fix:
function availabilityTimeToUTC(
dayOfWeek: number,
time: string,
timezone: string,
): { dayOfWeek: number; utcTime: Date } {
const referenceDate = new Date("2024-01-01");
referenceDate.setDate(referenceDate.getDate() + dayOfWeek);
const [hours, minutes] = time.split(":").map(Number);
referenceDate.setHours(hours, minutes, 0, 0);
const utcDate = fromZonedTime(referenceDate, timezone);
return {
dayOfWeek: utcDate.getUTCDay(),
utcTime: new Date(`1970-01-01T${format(utcDate, "HH:mm")}:00Z`),
};
}
const start = availabilityTimeToUTC(1, "00:00", "America/Los_Angeles");
const end = availabilityTimeToUTC(1, "17:00", "America/Los_Angeles");
await db.availability.createMany({
data: [
{
userId: emilyId,
dayOfWeek: start.dayOfWeek,
startTime: start.utcTime,
endTime: end.utcTime,
timezone: "America/Los_Angeles",
},
],
});
Result: Seeding succeeds, times validate correctly across all timezones.
Notes
When This Pattern is Needed
This pattern is essential for:
- Weekly schedules stored in UTC (e.g., "Every Monday 9-5")
- Recurring events with timezone support
- Availability blocks for booking systems
- Business hours with multi-timezone support
- Appointment scheduling systems
When to Use Exclusive End Dates
Use exclusive end dates (next day) for:
- Single-day events or blockouts
- Date ranges where start = end doesn't make semantic sense
- Schemas with
effectiveFrom < effectiveUntil constraints
- Calendar events with "all day" semantics
Timezone Best Practices
- Always store times in UTC in the database
- Track day-of-week in UTC if using recurring schedules
- Convert at display time to user's local timezone
- Use IANA timezone identifiers (e.g., "America/Los_Angeles", not "PST")
- Test with multiple timezones including negative offsets
- Document timezone assumptions in schema comments
Common Pitfalls
- Extracting only time portion without day: Loses day-of-week information
- Hardcoding day-of-week: Ignores timezone conversion effects
- Using inclusive end dates: Fails validation when start = end
- Not testing negative UTC offsets: Most bugs appear in UTC-X timezones
- Assuming same day after conversion: Early/late times often shift days
Database Schema Considerations
If your schema stores times without dates (time-of-day format), ensure:
- Day-of-week field matches the UTC day, not local day
- Validation constraints account for timezone shifts
- Documentation explains the UTC storage pattern
- Example data includes cross-timezone test cases
References