Stop scheduling meetings at times that don't exist
Every calendar app, booking platform, and scheduling tool ships the same silent bug class: a user picks a local time, your code stores it, and twice a year the event fires at the wrong hour because of daylight saving. ChronoShield catches the bug at the boundary — before the bad row hits your database.
User picks 2:30 AM March 8 in New York. That time doesn't exist on March 8. Most calendar apps silently shift to 3:30 AM and store it.
Standup happens an hour late. Two engineers no-show.
User picks 1:30 AM November 1. That time happens twice that day. Your system picks one offset, the calendar UI picks the other.
User and host show up to different meetings, an hour apart.
Why this matters for the business, not just the code
- Customer trust: the moment a user shows up to a meeting an hour late because your scheduler glitched, they're done. They blame the product, not DST.
- Support volume: the bug fires twice a year, predictably. That's a recurring spike of "my meeting was at the wrong time" tickets you can't reproduce or explain.
- Hard to debug: by the time the user complains, the bad event is in your database and the calendar event has already fired. There's no error log because the library didn't think it failed.
- Multi-tenant blast radius: one user with the bug doesn't notice. A thousand users with the bug means you trend on Twitter on March 8.
The integration in three lines
Add a single API call inside your booking-form handler, before the INSERT. That's the whole change.
// In your booking submit handler:
const check = await chronoshield.validate({ local_datetime, time_zone });
if (check.status !== "valid") return showFixModal(check);
// Otherwise: safe. Resolve to UTC and store.
const utc = await chronoshield.resolve({ local_datetime, time_zone });
await db.events.create({ instant_utc: utc.instant_utc, local_datetime, time_zone });
Request & response
POST /v1/datetime/validate
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"local_datetime": "2026-03-08T02:30:00",
"time_zone": "America/New_York"
}
{
"status": "invalid",
"reason_code": "DST_GAP",
"message": "This time does not exist due to DST transition.",
"suggested_fixes": [
{ "strategy": "next_valid_time", "local_datetime": "2026-03-08T03:00:00" },
{ "strategy": "previous_valid_time", "local_datetime": "2026-03-08T01:59:59" }
]
}
Recommended implementation pattern
For a scheduling app, the canonical pattern stores three columns per event: instant_utc (for sorting and triggering), local_datetime (the user's original input), and time_zone (their IANA zone).
// 1. User submits booking form with local_datetime + time_zone
// 2. Validate at the input boundary
const check = await chronoshield.validate({ local_datetime, time_zone });
if (check.reason_code === "DST_GAP") {
// Don't shift silently. Show suggested_fixes[0] as a "did you mean?" prompt.
return res.status(400).json({ error: "gap", suggested: check.suggested_fixes });
}
if (check.reason_code === "DST_OVERLAP") {
// Two valid instants. Ask the user OR apply your default policy.
return res.status(400).json({ error: "overlap", possible_instants: check.possible_instants });
}
if (check.status === "invalid") {
return res.status(400).json({ error: check.reason_code, message: check.message });
}
// 3. Validated. Resolve to UTC for storage.
const utc = await chronoshield.resolve({
local_datetime, time_zone,
resolution_policy: { ambiguous: "earlier", invalid: "next_valid_time" },
});
// 4. Store all three. UTC for sorting/triggering, local + tz for re-rendering.
await db.events.create({
instant_utc: utc.instant_utc,
local_datetime,
time_zone,
});
When to use validate vs. resolve vs. convert
| Scenario | Endpoint | Why |
|---|---|---|
| User submits a booking form with local time + tz | /validate first, then /resolve |
Validate to catch bad input; resolve to get the UTC instant for storage. |
| Display a stored event in a viewer's timezone | /convert |
UTC → local. Unambiguous — one UTC instant maps to one local time per zone. |
| Importing 50 events from an .ics file | /batch |
One round trip. Per-item failures don't fail the whole import. |
| Recurring meeting "every Wed 9am Eastern" | /resolve per occurrence |
Recompute each occurrence's UTC fresh from the local-time rule. Don't add 24h to the previous one — DST will drift it. |
| User edits the time of an existing event | /validate + /resolve |
Same as initial creation. Update all three columns: instant_utc, local_datetime, time_zone. |
Cross-timezone meetings
A host in Berlin invites a guest in Chicago for a 14:00 Berlin time meeting on the day Berlin transitions out of DST but Chicago hasn't yet. The two transitions don't happen on the same day. If your scheduler doesn't pick a single source-of-truth zone for the event, you can end up with the host and guest seeing times that differ by an hour from each other for the entire week.
The pattern: store the event in the host's zone (or whichever zone the user picked it in), resolve once to a UTC instant, and convert to each viewer's zone on display via /convert. ChronoShield's bundled tzdata gives every viewer the same answer.
Recurring events the right way
Recurring meetings store a rule, not a list of UTC instants. The rule is "every weekday at 9am in America/New_York." The next fire time is recomputed from the rule each iteration, freshly resolved through /resolve.
Anti-pattern: precomputing 90 days of UTC instants and storing them as a list. The first DST transition in that 90-day window will drift them by an hour and you'll spend a Friday afternoon re-explaining DST to a customer's CTO.
Try it in two minutes
Get a free API key (no card), POST a booking-form datetime to /v1/datetime/validate, and watch the bug surface as a structured error code your app can handle deterministically.