{"record":{"id":"6a4b91f37bd9c3e5","repo":"koala73/worldmonitor","slug":"pro-required-6a4b91","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/notificationChannels.ts","lineNumber":69,"sourceCode":"  userId: string,\n): Promise<boolean> {\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  return tier >= 1;\n}\n\nasync function assertProEntitlement(\n  ctx: MutationCtx,\n  userId: string,\n): Promise<void> {\n  if (!(await hasProEntitlement(ctx, userId))) {\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/**\n * Queue a first-connect welcome outside the relay HTTP request lifecycle.\n *\n * The mutation that creates the channel schedules this action in the same\n * Convex transaction as the channel insert. A Vercel-to-Convex timeout can\n * therefore hide the mutation response without losing the welcome event.\n */\nexport const queueChannelWelcome = internalAction({\n  args: {\n    userId: v.string(),\n    channelType: channelTypeValidator,","sourceCodeStart":51,"sourceCodeEnd":87,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/notificationChannels.ts#L51-L87","documentation":"Thrown by `assertProEntitlement` (used by notification-channel mutations) when the user's entitlement row is missing, expired (`validUntil < now`), or has `features.tier < 1`. Notifications are a Pro-only feature and the gate is enforced at the Convex write boundary so callers cannot bypass the edge API gate. Carries object data `{ code: \"PRO_REQUIRED\", message: \"...\" }` — note this uses `code` (not `kind` like followedCountries), so the client branches on `err.data.code`. The helper `hasProEntitlement` reads the `entitlements` table by userId.","triggerScenarios":"Calling `setChannel`, `deleteChannel`, or any notification mutation as a free-tier user; an entitlement that expired (`validUntil` in the past); a missing entitlement row for a never-subscribed user; a webhook-sync race where the user just upgraded but the row isn't written yet.","commonSituations":"A free user finds the notifications settings panel; a Pro user's subscription lapsed and the entitlement row expired but the UI still shows the panel; a billing webhook delay after checkout; a test account without a seeded entitlement.","solutions":["Gate the notifications UI on the user's entitlement tier (>= 1) client-side; hide the panel for free users.","On `err.data.code === \"PRO_REQUIRED\"`, show an upgrade prompt and disable the channel action.","After a Pro upgrade, await the entitlement webhook/sync before allowing channel creation.","Ensure test/dev accounts have a seeded entitlement row with future `validUntil` and `tier >= 1`."],"exampleFix":"// before\nawait convex.mutation(api.notificationChannels.setChannel, { channelType: \"telegram\", chatId });\n\n// after — gate on entitlement\nconst { tier } = useEntitlement();\nif (tier < 1) { showUpgradeModal(); return; }\ntry {\n  await convex.mutation(api.notificationChannels.setChannel, { channelType: \"telegram\", chatId });\n} catch (err) {\n  if (err.data?.code === \"PRO_REQUIRED\") showUpgradeModal();\n  else throw err;\n}","handlingStrategy":"validation","validationCode":"const { tier } = useEntitlement();\nif (!tier || tier < 1) { showUpgradeModal(); return; }","typeGuard":"function hasProTier(ent: { features: { tier: number } } | null): boolean {\n  return !!ent && ent.features.tier >= 1;\n}","tryCatchPattern":"try {\n  await convex.mutation(api.notificationChannels.setChannel, { channelType, chatId });\n} catch (err) {\n  if (err.data?.code === \"PRO_REQUIRED\") showUpgradeModal();\n  else throw err;\n}","preventionTips":["Gate the notifications UI on entitlement tier >= 1 client-side.","This module uses `code` (not `kind`) — branch on err.data.code.","After a Pro upgrade, await the entitlement sync before allowing channel creation.","Seed test accounts with a future-dated tier >= 1 entitlement.","Note the check requires validUntil >= now, not just tier >= 1."],"tags":["auth","convex","entitlement","pro","notifications"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}