Back to docs
Recipe

Quiet hours design

How Meridian enforces user-defined quiet windows so alerts never fire when you're sleeping, streaming, or in focus mode.

The problem

A detection fires at 3:12 AM. The user is asleep. The alert is correct but the timing is wrong — it erodes trust and trains people to ignore notifications. We needed a scheduling layer that is simple to configure, impossible to accidentally bypass, and cheap to evaluate at scale.

Data model

Each user profile stores an ordered list of quiet-hour windows. Every window has a start time, end time, days-of-week bitmask, and a priority. Windows can overlap — the highest-priority window wins. The default window (priority 0) is always-active, so deleting all custom windows restores 24/7 alerting.

{
  "windows": [
    {
      "id": "sleep",
      "start": "22:00",
      "end": "07:00",
      "days": 127,
      "priority": 10
    },
    {
      "id": "focus-block",
      "start": "09:00",
      "end": "11:30",
      "days": 31,
      "priority": 20
    }
  ]
}

Evaluation path

When an alert is ready to dispatch, the notifier calls a single pure function: isQuiet(now, windows). It converts the current UTC time into the user's configured timezone, checks the day-of-week bit, and walks the window list in priority-descending order. The first window whose interval contains the local time short-circuits the check. If no window matches, the default window returns false — alert proceeds.

Edge cases we handle

  • Midnight wraparound: windows like 22:00–07:00 are split into two intervals internally so the comparison stays correct.
  • DST transitions: we evaluate against the user's IANA timezone, not a fixed offset, so spring-forward and fall-back gaps are handled by the platform.
  • Overlapping windows: priority ordering ensures deterministic behavior. A high-priority focus block inside a sleep window suppresses alerts during the focus block even though sleep would also match.
  • Empty window list: treated as always-active. New accounts ship with zero windows and get full alerting by default.

Why it works at scale

The check is O(n) on a list that rarely exceeds five windows. No database queries, no cron jobs, no state machines. The notifier calls isQuiet inline before every dispatch. If the user is in a quiet window, the alert is queued to a deferred bucket and re-evaluated when the window closes. This keeps the hot path under 200 µs and avoids building a separate scheduler service.