koala73/worldmonitor · error · ConvexError
PRO_REQUIRED
PRO_REQUIRED
Error message
Notifications are a PRO feature. Upgrade to enable real-time and digest alerts.
What it means
assertProEntitlement (convex/alertRules.ts:36) is the layer-2 write-path entitlement gate called by every alert-rule mutation (setAlertRules, setDigestSettings, setQuietHours, setNotificationConfigForUser). It reads the user's 'entitlements' row and treats a missing row or validUntil < Date.now() as tier 0 (free). Any tier < 1 throws ConvexError with structured data { code: 'PRO_REQUIRED' } so the client can route to the upgrade flow instead of surfacing a generic 500.
Solutions
- If the caller is a legitimate end user: catch the ConvexError, inspect data.code === 'PRO_REQUIRED', and route to the upgrade screen — do not retry the mutation.
- If the user should be PRO: repair their entitlements document (features.tier >= 1 and validUntil >= Date.now()). An expired row is treated as tier 0 even if tier was 1.
- In tests and E2E runs: seed a valid entitlement row for the test user's Clerk subject before invoking any alert-rule mutation.
- For trusted operator scripts that must manage free-tier rows, use the intentionally ungated *ForUser internal mutations (setAlertRulesForUser, setQuietHoursForUser) — note setNotificationConfigForUser IS gated and will still throw.
Example fix
// before
await mutateAPI.alertRules.setAlertRules({ variant: 'default', enabled: true, eventTypes: ['military'], channels: ['email'] });
// ConvexError: PRO_REQUIRED
// after
import { ConvexError } from 'convex/values';
try {
await mutateAPI.alertRules.setAlertRules({ variant: 'default', enabled: true, eventTypes: ['military'], channels: ['email'] });
} catch (err) {
if (err instanceof ConvexError && (err.data as { code?: string })?.code === 'PRO_REQUIRED') {
router.push('/upgrade?from=alerts');
return;
}
throw err;
} Defensive patterns
Strategy: try-catch
Validate before calling
// Optional UI pre-gate using your cached subscription state; the server check remains authoritative.
if (!user?.isPro) {
openUpgradeDialog('Notifications are a PRO feature');
return;
}
await mutateAPI.alertRules.setAlertRules(args); Try / catch
import { ConvexError } from 'convex/values';
function isProRequired(err: unknown): boolean {
return (
err instanceof ConvexError &&
typeof err.data === 'object' &&
err.data !== null &&
(err.data as { code?: string }).code === 'PRO_REQUIRED'
);
}
try {
await mutateAPI.alertRules.setAlertRules(args);
} catch (err) {
if (isProRequired(err)) { router.push('/upgrade?from=alerts'); return; }
throw err;
} Prevention
- Gate the alert-settings UI behind the same tier state the server enforces (tier >= 1 AND validUntil not expired).
- Check entitlement freshness, not just the tier field — expired rows behave as tier 0.
- Seed entitlements documents for every test user before alert-mutation suites.
- Map data.code === 'PRO_REQUIRED' to the upgrade flow; never show it as a generic error or retry it.
When it happens
Trigger: Calling api.alertRules.setAlertRules / setDigestSettings / setQuietHours as a free-tier user; calling the public 'set-notification-config' HTTP action (which forwards to the gated setNotificationConfigForUser) for a user whose entitlement expired (validUntil in the past) or who has no entitlements document; E2E tests that sign in a fresh Clerk user without seeding an entitlement row.
Common situations: A patched client or old UI build bypassing the layer-1 paywall (the comment records a 2026-04-28 audit finding 7 of 28 enabled rules belonged to free-tier users); subscriptions that lapsed after card failure or refund (validUntil passes silently); test environments where only Clerk auth was wired up, not entitlements.
Related errors
- Account changed while creating the embed key. Try again.
- Authentication unavailable while loading Business Pro…
- Authentication unavailable while loading embed keys. Try…
- Authentication unavailable while loading MCP clients. Try…
- HTTP
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/976857570efbcf2c.
Report an issue: GitHub.
Appendix: source
Thrown at convex/alertRules.ts:49
*
* Kept inline (not imported from entitlements.ts) for security-review
* readability: every alertRules mutation that calls this should be
* trivially auditable in one file.
*/
async function assertProEntitlement(
ctx: MutationCtx,
userId: string,
): Promise<void> {
const entitlement = await ctx.db
.query("entitlements")
.withIndex("by_userId", (q) => q.eq("userId", userId))
.first();
const tier =
entitlement && entitlement.validUntil >= Date.now()
? entitlement.features.tier
: 0;
if (tier < 1) {
throw new ConvexError({
code: "PRO_REQUIRED",
message:
"Notifications are a PRO feature. Upgrade to enable real-time and digest alerts.",
});
}
}
// Cross-field invariant enforcement for (digestMode, sensitivity).
//
// Tightened rule (2026-04-27): real-time delivery is now reserved for
// `critical`-tier events only. `(realtime, all)` and `(realtime, high)` are
// both forbidden. Anything below `critical` lives in a digest cadence
// (daily / twice_daily / weekly).
//
// Why tighter: even on `(realtime, high)`, `high`-severity events fire
// frequently enough on busy days to overload an inbox (severe weather,
// market moves, geopolitics). Real-time is for "interrupt me NOW" content
// only — i.e. genuinely critical. High events still reach the user, justView on GitHub (pinned to eeab0a219f)