ProjectR Docs
Architecture

Notification Pipeline

The single enqueueNotification path: policies, quiet hours, push delivery.

The one rule

Every producer calls the internal mutation enqueueNotification (or enqueueNotificationHelper from within another mutation). Never schedule a push send directly (ctx.scheduler.runAfter to the push action) — that bypasses preferences, quiet hours, unread counting, and the retry pipeline.

Policy evaluation

evaluatePolicy is pure and runs inside the enqueueing mutation, so the preferences read and the insert share one snapshot:

  • No preferences row → allow everything (default opt-in).
  • pushEnabled: false suppresses push; inAppEnabled: false suppresses in-app.
  • A disabled type suppresses push only (in-app still shows).
  • Quiet hours suppress push only (timezone-aware; evaluated at enqueue time).

The result is stored on the row as policyResult.

Status vs channelStatus

  • status drives the pipeline: pending → sent | failed.
  • channelStatus records per-channel truth: inserting the row is in-app delivery (inApp: "delivered"); push starts pending and moves to sent | failed | suppressed.
  • If every channel is suppressed, status is sent — nothing to deliver.

Delivery and races

  • Push delivery is an internal action on a workpool (3 retries), idempotent on terminal status, sending per active Expo token.
  • The workpool enqueue writes an enqueuedAt claim; the 5-minute cron re-enqueues unclaimed rows whose scheduledAt passed. If the cron and a manual process race the same row, the two claims OCC-conflict and only one commits — deliberate duplicate-push protection.
  • The unread sharded counter increments only when in-app is an effective channel (countsTowardUnread), and decrements under the same condition — it cannot drift.

Mobile firewall

toMobileFormat() in convex/notification/queries.ts is the field firewall: new backend fields are invisible to the mobile app until explicitly added there.

The app's half of the path, from the permission pre-prompt to push token registration and the Android default channel, is on Mobile Notifications.

On this page