Navigation

TodoList due reminders

A TodoList item with a due date pushes a web-push reminder to its owner when it comes due — the first platform feature to reuse the notification infrastructure esbabbler already runs, and the thing that turns the TodoList type from a table into an actual todo product.

How it works

Reminder delivery adds no new Azure services: the Service Bus scheduled-message pattern (already used for scheduled messages) carries the timer, and the existing notification pipeline carries the notification.

  • Scheduling — after a TodoList saveResourceContent persists, the server reads the prior content blob and diffs due dates, enqueueing one scheduled Service Bus message per item whose (itemId, dueAt) is new or changed, still in the future, and not completed. A todo completed on the previous save counts as having no reminder, so reopening one enqueues its due date even if it was set while the todo was completed. The diff is fire-and-forget and best-effort: a failed enqueue logs and never fails the user's save, and an unreadable prior blob degrades to "no previous content" rather than blocking the save. Diffing keeps repeated saves from piling up duplicate reminders for an unchanged due date.
  • Fire-time verification is the consistency model — the reminder carries only { resourceId, itemId, dueAt }; there is no Postgres row backing it, so the scheduled message is the state. When it fires, SendTodoReminder re-reads the live content blob and drops the reminder if the item was deleted, re-dated or completed (a re-dated item enqueued its own fresh message at save time). This makes stale messages harmless, so saves never have to cancel previously enqueued ones.
  • Delivery — the handler publishes a TodoReminder notification and the shared pipeline does the rest (notifications): 『{item}』 is due reaches the owner's bell and every device they have subscribed. Clicking it opens the resource's Items blade (/resource-explorer/{id}/items). TodoLists are single-owner resources, so the recipient set is just the owner — no fan-out.
sequenceDiagram
  participant E as Items blade
  participant S as saveResourceContent
  participant SB as Service Bus (scheduled)
  participant F as SendTodoReminder
  participant B as Content blob
  participant EG as Event Grid
  participant P as ProcessNotification

  E->>S: save items (due dates diffed against prior blob)
  S--)SB: enqueue resourceId, itemId, dueAt scheduled at dueAt (best-effort)
  Note over SB: one scheduled message per new or changed (item, dueAt)
  SB->>F: fires at dueAt
  F->>B: re-read content blob
  Note over F,B: item gone, dueAt changed or completed drops the reminder
  F->>EG: publishNotification — TodoReminder for the owner
  EG->>P: 『item』 is due → bell row + owner's devices

Data model

None in Postgres. The TodoList item already carries dueAt (the Items and Calendar blades render it), and the reminder is stateless — the scheduled Service Bus message holds the whole payload and the blob re-read is the truth check. The todo-reminders Service Bus queue is provisioned alongside the existing scheduled-message-jobs queue in apps/infra.

Key files

FileRole
apps/web/server/services/resource/todoList/scheduleTodoReminders.tsPost-save due-date diff and per-item enqueue
apps/web/server/services/resource/ResourceAfterSaveContentMap.tsRegisters the diff as TodoList's after-save hook
apps/web/server/services/resource/runAfterSaveResourceContent.tsFires the registered hook, fire-and-forget
apps/web/server/services/resource/saveResourceContent.tsThe one content write that runs the hook
packages/db/src/services/azure/serviceBus/enqueueTodoReminder.tsSchedules the Service Bus message at dueAt
packages/db-schema/src/models/azure/queue/TodoReminderQueueMessage.tsThe { resourceId, itemId, dueAt } message schema
apps/functions/src/functions/sendTodoReminder.tsQueue-trigger registration for SendTodoReminder
apps/functions/src/handlers/sendTodoReminderHandler.tsRe-reads the blob, verifies the item, and publishes
apps/functions/src/services/notification/resolveNotification.tsTurns the published reminder into its copy, deep link and recipient

Notes

  • At-least-once delivery: a duplicate fire re-verifies against the blob and notifies twice in the worst case — acceptable for reminders, and cheaper than a dedup table.
  • A due date toggled away and back across saves re-enqueues a reminder for the same timestamp that fire-time verification cannot distinguish from the original — an accepted duplicate (one extra push). Service Bus duplicate detection would collapse it via a deterministic message id, but requires the Standard tier; the namespaces run Basic.
  • Reminder timing is exactly dueAt in this first cut. Lead-time offsets ("remind me 1h before") are a follow-up dueAt-relative field, not a reason to build preference UI now.

Details

Command palette

Keyboard shortcuts