{"record":{"id":"976857570efbcf2c","repo":"koala73/worldmonitor","slug":"pro-required","errorCode":"PRO_REQUIRED","errorMessage":"Notifications are a PRO feature. Upgrade to enable real-time and digest alerts.","messagePattern":"Notifications are a PRO feature\\. Upgrade to enable real-time and digest alerts\\.","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/alertRules.ts","lineNumber":49,"sourceCode":" *\n * Kept inline (not imported from entitlements.ts) for security-review\n * readability: every alertRules mutation that calls this should be\n * trivially auditable in one file.\n */\nasync function assertProEntitlement(\n  ctx: MutationCtx,\n  userId: string,\n): Promise<void> {\n  const entitlement = await ctx.db\n    .query(\"entitlements\")\n    .withIndex(\"by_userId\", (q) => q.eq(\"userId\", userId))\n    .first();\n  const tier =\n    entitlement && entitlement.validUntil >= Date.now()\n      ? entitlement.features.tier\n      : 0;\n  if (tier < 1) {\n    throw new ConvexError({\n      code: \"PRO_REQUIRED\",\n      message:\n        \"Notifications are a PRO feature. Upgrade to enable real-time and digest alerts.\",\n    });\n  }\n}\n\n// Cross-field invariant enforcement for (digestMode, sensitivity).\n//\n// Tightened rule (2026-04-27): real-time delivery is now reserved for\n// `critical`-tier events only. `(realtime, all)` and `(realtime, high)` are\n// both forbidden. Anything below `critical` lives in a digest cadence\n// (daily / twice_daily / weekly).\n//\n// Why tighter: even on `(realtime, high)`, `high`-severity events fire\n// frequently enough on busy days to overload an inbox (severe weather,\n// market moves, geopolitics). Real-time is for \"interrupt me NOW\" content\n// only — i.e. genuinely critical. High events still reach the user, just","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/alertRules.ts#L31-L67","documentation":"Thrown by assertProEntitled() in the Convex alertRules mutation when the authenticated user's entitlement tier is below 1 (not PRO). The function queries the entitlements table, checks validUntil is in the future, reads the tier from features.tier, and if tier < 1 throws a ConvexError with code 'PRO_REQUIRED'. Notifications (real-time and digest alerts) are gated behind PRO, so any mutation that creates or modifies alert rules must pass this check first.","triggerScenarios":"A free-tier (tier 0) or unentitled user calling a Convex mutation that invokes assertProEntitled — e.g. creating an alert rule, enabling notifications, or modifying digest settings. The user has either never subscribed to PRO, their subscription expired (validUntil < Date.now()), or their entitlements record is missing entirely (entitlement query returns null, tier defaults to 0).","commonSituations":"A free-tier user trying to set up alerts after the free preview ended; a lapsed PRO subscription (validUntil expired); a missing entitlements record for a user who should have PRO (sync failure between the billing system and Convex); a new user who has not yet subscribed.","solutions":["Upgrade to PRO — the error message directs the user to upgrade; once the entitlements table reflects tier >= 1 with a valid validUntil, the mutation succeeds.","If the user IS a paying PRO subscriber, check that their entitlements record exists in Convex and validUntil is in the future — a billing-sync failure may have left the record stale or missing.","If validUntil recently expired, verify the subscription renewal processed and the entitlements webhook/sync ran.","For testing, seed the entitlements table with a valid tier-1 record for the test user."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Check entitlement before calling the alert-rule mutation\nconst entitlement = await convex.query(api.entitlements.getUserEntitlement, { userId });\nconst tier = entitlement && entitlement.validUntil >= Date.now() ? entitlement.features.tier : 0;\nif (tier < 1) {\n  // Show upgrade prompt instead of calling the mutation\n  showUpgradePrompt('Notifications are a PRO feature');\n  return;\n}\n// Safe to proceed with the mutation\nawait convex.mutation(api.alertRules.createRule, { userId, ...rule });","typeGuard":"function isProRequiredError(e: unknown): e is { code: string; message: string } & Error {\n  return e instanceof Error && (e as any).data?.code === 'PRO_REQUIRED';\n}","tryCatchPattern":"try {\n  await convex.mutation(api.alertRules.createRule, { userId, ...rule });\n} catch (e) {\n  if (isProRequiredError(e)) {\n    // Show upgrade UI / redirect to billing\n    redirectToBilling();\n  } else throw e;\n}","preventionTips":["Query the user's entitlement (tier >= 1, validUntil in the future) BEFORE showing notification-setup UI.","Gate the notification/settings UI behind the entitlement check so users never reach the mutation unentitled.","Handle subscription-expiry gracefully by checking validUntil, not just tier.","Ensure the billing-to-Convex entitlement sync is reliable so PRO subscribers always have valid records."],"tags":["convex","authorization","pro-required","entitlements","notifications"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}