koala73/worldmonitor · error · ConvexError

INCOMPATIBLE_DELIVERY

INCOMPATIBLE_DELIVERY

Error message

Real-time delivery is for Critical events only. To receive High or All events, choose a digest cadence (Daily, Twice daily, or Weekly).

What it means

Thrown by assertCompatibleDeliveryMode when an alert rule resolves to digestMode 'realtime' combined with sensitivity 'all' or 'high'. After the 2026-04-27 tightening, real-time delivery is reserved for critical-tier events only because high/all fire too frequently for an inbox. Callers must pick a digest cadence (daily, twice_daily, weekly) for non-critical sensitivities.

Source

Thrown at convex/alertRules.ts:89

// existing.sensitivity when caller omits the field (no silent narrowing of
// digest users).
function resolveEffectivePair(args: {
  incomingDigestMode?: DigestMode;
  incomingSensitivity?: Sensitivity;
  existing?: { digestMode?: DigestMode | string; sensitivity?: Sensitivity | string };
}): { digestMode: DigestMode; sensitivity: Sensitivity } {
  const digestMode = (args.incomingDigestMode
    ?? (args.existing?.digestMode as DigestMode | undefined)
    ?? "realtime");
  const sensitivity = (args.incomingSensitivity
    ?? (args.existing?.sensitivity as Sensitivity | undefined)
    ?? "critical"); // insert-only default — patch path never includes sensitivity unless caller passed it
  return { digestMode, sensitivity };
}

function assertCompatibleDeliveryMode(pair: { digestMode: DigestMode; sensitivity: Sensitivity }) {
  if (pair.digestMode === "realtime" && (pair.sensitivity === "all" || pair.sensitivity === "high")) {
    throw new ConvexError({
      code: "INCOMPATIBLE_DELIVERY",
      message:
        "Real-time delivery is for Critical events only. " +
        "To receive High or All events, choose a digest cadence (Daily, Twice daily, or Weekly).",
    });
  }
}

// Defensive ceiling against patched-client abuse — there are ~250 ISO-3166
// countries; 50 is more than any real user opts into and well below any
// validator/storage limit.
const COUNTRIES_MAX = 50;

/**
 * Shape-validate + normalize an inbound `countries` array.
 *  - trim each entry
 *  - uppercase
 *  - keep only ASCII A-Z 2-letter shapes (`^[A-Z]{2}$`); silently drop the rest

View on GitHub (pinned to ffec79ac33)

Solutions

  1. When sensitivity is 'high' or 'all', set digestMode to 'daily', 'twice_daily', or 'weekly' instead of 'realtime'.
  2. If real-time is required, set sensitivity to 'critical'.
  3. For atomic UI transitions from daily+all to realtime, use setNotificationConfigForUser which updates both fields in one transaction instead of separate setDigestSettings + setAlertRules calls that trip the validator mid-transition.
  4. Run the migration referenced in docs/archive/plans/forbid-realtime-all-events.md to drain pre-existing forbidden rows.

Example fix

// before
await setDigestSettings(ctx, { variant, digestMode: 'realtime' }); // existing.sensitivity === 'all'
// after
await setNotificationConfigForUser(ctx, {
  userId, variant,
  digestMode: 'realtime',
  sensitivity: 'critical', // atomic update keeps the pair consistent
});
Defensive patterns

Strategy: validation

Validate before calling

function isValidPair(digestMode, sensitivity) {
  if (digestMode === 'realtime' && (sensitivity === 'all' || sensitivity === 'high')) return false;
  return true;
}
// before submit:
if (!isValidPair(effectiveDigestMode, effectiveSensitivity)) {
  // surface guidance: pick a digest cadence or use 'critical'
}

Type guard

type DigestMode = 'realtime' | 'daily' | 'twice_daily' | 'weekly';
type Sensitivity = 'all' | 'high' | 'critical';
function isCompatiblePair(d: DigestMode, s: Sensitivity): boolean {
  return !(d === 'realtime' && (s === 'all' || s === 'high'));
}

Try / catch

try {
  await setNotificationConfigForUser(ctx, args);
} catch (e) {
  if (e instanceof ConvexError && e.data?.code === 'INCOMPATIBLE_DELIVERY') {
    // show user: choose digest cadence OR set sensitivity to 'critical'
  } else throw e;
}

Prevention

When it happens

Trigger: Calling setAlertRules with sensitivity 'high' or 'all' while the existing rule (or default) has digestMode 'realtime'; calling setDigestSettings with digestMode 'realtime' on an existing rule whose stored sensitivity is 'high'/'all'; calling setNotificationConfigForUser with both digestMode:'realtime' and sensitivity:'high'/'all'. The check fires in assertCompatibleDeliveryMode via resolveEffectivePair after merging incoming + existing values.

Common situations: Migrating a legacy rule that previously allowed realtime+high; a UI that lets the user change sensitivity independently of digest mode without re-validating the pair; an admin/migration script invoking *ForUser internal mutations on pre-tightening rows.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/7842f8209a65a463. Report an issue: GitHub.