{"record":{"id":"be37341bed6b48a0","repo":"koala73/worldmonitor","slug":"authenticated-account-changed-during-push-setup","errorCode":null,"errorMessage":"Authenticated account changed during push setup","messagePattern":"Authenticated account changed during push setup","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/push-notifications.ts","lineNumber":99,"sourceCode":"  auth: string;\n  userAgent: string;\n}\n\nfunction subscriptionToPayload(sub: PushSubscription): SubscriptionPayload | null {\n  const p256dh = arrayBufferToBase64(sub.getKey('p256dh'));\n  const auth = arrayBufferToBase64(sub.getKey('auth'));\n  if (!p256dh || !auth || !sub.endpoint) return null;\n  return {\n    endpoint: sub.endpoint,\n    p256dh,\n    auth,\n    userAgent: typeof navigator !== 'undefined' ? navigator.userAgent.slice(0, 200) : '',\n  };\n}\n\nfunction assertExpectedAccount(expectedUserId?: string): void {\n  if (expectedUserId && getCurrentClerkUser()?.id !== expectedUserId) {\n    throw new Error('Authenticated account changed during push setup');\n  }\n}\n\nasync function authFetch(\n  path: string,\n  init: RequestInit,\n  expectedUserId?: string,\n): Promise<Response> {\n  assertExpectedAccount(expectedUserId);\n  const token = await getClerkToken();\n  if (!token) throw new Error('Not authenticated');\n  assertExpectedAccount(expectedUserId);\n  return fetch(path, {\n    ...init,\n    headers: {\n      'Content-Type': 'application/json',\n      ...(init.headers ?? {}),\n      Authorization: `Bearer ${token}`,","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/src/services/push-notifications.ts#L81-L117","documentation":"Thrown by assertExpectedAccount() in src/services/push-notifications.ts, which guards every step of authFetch and subscribeToPush. The flow captures an expectedUserId when it starts; if Clerk's current user (getCurrentClerkUser()?.id) no longer matches at any checkpoint, the operation aborts. This prevents registering a push subscription — which carries browser-level credentials — against the wrong user's row after a mid-flow account switch.","triggerScenarios":"User signs out and signs in as another account in a different tab while the push-enable flow is between its checkpoints; an account switcher changes the Clerk user during the await of getClerkToken(); a component re-invokes subscribeToPush with a stale expectedUserId captured before the switch.","commonSituations":"Shared machines where users swap accounts with settings tabs open; multi-account QA workflows; Clerk session events firing mid-subscription on slow networks.","solutions":["Discard the in-flight flow entirely and restart it with the new user's id — never resume a crossed flow.","Pass expectedUserId from the component that owns the current session (re-read at flow start), not a value cached across renders.","Subscribe to Clerk user-change events and cancel/abort pending push setup on change.","On catch, re-check getCurrentClerkUser()?.id and re-anchor the UI to the new session before retrying."],"exampleFix":"// before\nawait subscribeToPush(capturedUserId);\n\n// after\ntry {\n  await subscribeToPush(capturedUserId);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Authenticated account changed during push setup') {\n    const currentId = getCurrentClerkUser()?.id;\n    if (currentId) await subscribeToPush(currentId);\n    return;\n  }\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"import { getCurrentClerkUser } from '@/services/clerk';\n\nconst expectedUserId = getCurrentClerkUser()?.id;\nif (!expectedUserId) return;\nawait subscribeToPush(expectedUserId); // captured at flow start, re-checked by every checkpoint","typeGuard":"function isAccountSwitchAbort(err: unknown): boolean {\n  return err instanceof Error && err.message === 'Authenticated account changed during push setup';\n}","tryCatchPattern":"try {\n  await subscribeToPush(expectedUserId);\n} catch (err) {\n  if (isAccountSwitchAbort(err)) {\n    cancelPushSetup(); // discard the crossed flow entirely\n    const currentId = getCurrentClerkUser()?.id;\n    if (currentId) restartPushSetup(currentId);\n    return;\n  }\n  throw err;\n}","preventionTips":["Capture expectedUserId at flow start from the live session, never from stale component props.","Abort in-flight push setup on Clerk user-change events.","Never resume a crossed-account flow — restart it anchored to the new user."],"tags":["clerk","session-mismatch","push-notifications","account-switch","race-condition"],"backgroundTag":"session-user-mismatch","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}