Skip to content

Scheduling & Reminders

The calendar (/calendar) tracks three things: personal events (including OA dates), tasks, and the notifications the system generates about them. Data lives per-user in users/{uid}/events, tasks, and notifications; useEvents / useTasks / useNotifications keep the UI live via onSnapshot. EventModal is where OA events get their reminder windows.

Reminder windows

An OA event opts into any of four reminder emails: 7 days, 3 days, 1 day, and 1 hour before start (d7 / d3 / d1 / h1). Sent state is recorded per-window on the event (remindersSent flags), so each window fires at most once.

Why a Worker, not GitHub Actions

The first implementation ran scripts/send-reminders.mjs on a GitHub Actions cron. GitHub's scheduled workflows are best-effort — under load they start late or silently skip, which is disqualifying for an "OA starts in 1 hour" email. The system was rebuilt as a dedicated Cloudflare Worker (worker-reminders/) on a per-minute cron trigger, which is reliable.

The Node script still exists and shares field conventions with the Worker — useful for manual backfills and local testing — but production sending is Worker-only.

Exactly-once under concurrency

A per-minute cron means overlapping invocations are possible (slow run + next tick). Deduplication is transactional:

  1. The Worker queries upcoming events whose windows are due and unsent.
  2. For each candidate it writes an optimistic claim (reminderClaims) using a Firestore precondition — only one invocation wins a given event+window.
  3. The winner sends via Resend, then sets the remindersSent.<window> flag.
  4. A send failure releases the claim so the next tick retries; a success is permanent.

The Worker talks to Firestore over the REST API with a service-account JWT (no SDK weight in a Worker) — the auth flow is documented in Cloudflare Workers.

The email-sync bug worth remembering

Reminders were silently skipping some users. Root cause: the Worker reads the recipient address from the users/{uid} profile document, but AuthContext only wrote email there on profile creation — accounts created before that field existed, or users who changed their auth email, had profiles with no/stale email, and the Worker (correctly) refused to send to nothing. Two-part fix:

  • AuthContext now syncs email on every sign-in, not just first creation.
  • scripts/backfill-emails.mjs was run once to patch existing profiles.

The lesson generalizes: any consumer that reads a mirrored field must ask "what keeps this mirror fresh?" — a one-time copy is a time bomb.

Dashboard integration

Today's events and due tasks surface on the dashboard's schedule widget; OA-type events also deep-link into the OA Simulator so "reminded → practicing" is one click.

Ediky Workflow — internal engineering documentation.