koala73/worldmonitor · warning

Notifications are blocked. Enable them in your browser…

Error message

Notifications are blocked. Enable them in your browser settings to continue.

What it means

Thrown by subscribeToPush() in src/services/push-notifications.ts when Notification.requestPermission() resolves to 'denied'. Denial is sticky: the user (or browser/enterprise policy) previously blocked notifications for this origin, and once denied, requestPermission() can never re-show the prompt — every subsequent call immediately returns 'denied'. Only the user can undo it via site permissions UI.

Solutions

  1. Before prompting, check Notification.permission === 'denied' and show instructions to unblock (padlock icon → Site settings → Notifications → Allow) instead of calling requestPermission().
  2. After the user changes the setting, they must reload or you must re-check Notification.permission — it will read 'granted' without a prompt.
  3. Never loop requestPermission() on denial; it cannot succeed programmatically.
  4. For enterprise-managed browsers, document that notifications require a policy exception.

Example fix

// before
await subscribeToPush(userId);

// after
if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
  showNotice('Notifications are blocked for this site. Enable them in your browser site settings, then reload.');
  return;
}
await subscribeToPush(userId);
Defensive patterns

Strategy: validation

Validate before calling

if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
  showUnblockInstructions('Click the padlock → Site settings → Notifications → Allow, then reload.');
  return;
}
await subscribeToPush(expectedUserId);

Type guard

function isPushPermissionDenied(): boolean {
  return typeof Notification !== 'undefined' && Notification.permission === 'denied';
}

Try / catch

try {
  await subscribeToPush(expectedUserId);
} catch (err) {
  if (err instanceof Error && err.message.startsWith('Notifications are blocked.')) {
    showUnblockInstructions(); // sticky denial — only the user can reset it
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: User clicked 'Block' on a previous prompt; OS-level or enterprise policy denies notifications; the site's permission was reset to block in browser settings; calling requestPermission() again after a prior denial (instant 'denied', no prompt).

Common situations: Users who dismissed early prompts with Block and later try to enable notifications from settings; corporate-managed browser profiles with notifications disabled; kiosk/restricted profiles.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/868dbc8176a2bd58. Report an issue: GitHub.

Appendix: source

Thrown at src/services/push-notifications.ts:139

/**
 * Ask permission (if needed), subscribe via pushManager, and register
 * the endpoint with the server. Resolves with the payload the server
 * accepted, or throws on cancel / denial / network failure.
 */
export async function subscribeToPush(expectedUserId?: string): Promise<SubscriptionPayload> {
  assertExpectedAccount(expectedUserId);
  if (!isWebPushSupported()) {
    throw new Error('Web push is not supported in this browser.');
  }
  const reg = await getRegistration();
  if (!reg) throw new Error('Service worker unavailable.');

  const perm = await Notification.requestPermission();
  if (perm !== 'granted') {
    throw new Error(
      perm === 'denied'
        ? 'Notifications are blocked. Enable them in your browser settings to continue.'
        : 'Notifications permission was not granted.',
    );
  }

  // Re-use an existing subscription if pushManager already has one
  // for this origin. Re-registering via POST keeps server state in
  // sync even when the browser has a stale subscription — this is
  // important after sign-out/sign-in, where the browser's push
  // identity persists but the Convex row does not.
  let sub = await reg.pushManager.getSubscription();
  if (!sub) {
    sub = await reg.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY) as BufferSource,
    });
  }

  const payload = subscriptionToPayload(sub);

View on GitHub (pinned to eeab0a219f)