{"record":{"id":"56a542440e4af645","repo":"koala73/worldmonitor","slug":"unauthenticated-56a542","errorCode":"UNAUTHENTICATED","errorMessage":"UNAUTHENTICATED","messagePattern":"UNAUTHENTICATED","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/followedCountries.ts","lineNumber":343,"sourceCode":" *   Tier 2 — denormalized user-meta count (Codex round-3 P0): under the\n *   shard lock, we safely lazy-create the per-user `followedCountriesUserMeta`\n *   row (kept additionally for the O(1) cap-check denominator and as the\n *   parity invariant `count === COUNT(followedCountries WHERE userId=X)`).\n *\n * Without Tier 1, two parallel first-ever mutations could both read\n * `meta=undefined`, both INSERT, and produce duplicate meta rows that\n * break the `.unique()` read AND re-open the cap-bypass window. With\n * Tier 1 in place, the brand-new-user race is closed deterministically.\n *\n * Errors are typed `ConvexError({kind, ...})` with object data so callers\n * can branch on `err.data.kind` (memory:\n * `convex-error-string-data-strips-errordata-on-wire`).\n */\nexport const followCountry = mutation({\n  args: { country: v.string() },\n  handler: async (ctx, args): Promise<FollowMutationResult> => {\n    const identity = await ctx.auth.getUserIdentity();\n    if (!identity) throw new ConvexError({ kind: \"UNAUTHENTICATED\" });\n    const userId = identity.subject;\n\n    if (!isValidIso2(args.country)) {\n      throw new ConvexError({\n        kind: \"INVALID_COUNTRY\",\n        country: args.country,\n      });\n    }\n\n    // Tier-1 lock: pre-seeded shard row. Read at top, patch at end.\n    const shard = await readShardOrThrow(ctx, userId);\n\n    // Tier-2 read: per-user denormalized count (lazy-created, but safe\n    // under the shard lock above).\n    const { meta, count: currentCount } = await readUserMeta(ctx, userId);\n\n    const existingRow = await ctx.db\n      .query(\"followedCountries\")","sourceCodeStart":325,"sourceCodeEnd":361,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/followedCountries.ts#L325-L361","documentation":"Thrown by the `followCountry` mutation when `ctx.auth.getUserIdentity()` returns a falsy value — i.e. there is no authenticated Clerk/Convex session attached to the request. This is the auth gate before any country validation or cap logic. Carries object data `{ kind: \"UNAUTHENTICATED\" }`; branch on `err.data.kind`. Note this module uses `ctx.auth.getUserIdentity()` directly (NOT the `requireUserId` helper), so it does NOT get the dev-mode `DEV_USER_ID` fallback — in `convex dev` without a real session it still throws.","triggerScenarios":"Calling `api.followedCountries.followCountry` from a client without an active Clerk session; the session token expired between page load and the click; calling from a server context with no forwarded auth header; in `convex dev` without `CONVEX_IS_DEV=true` AND without a logged-in browser session.","commonSituations":"User's Clerk session expired (jwt expiry) while the dashboard tab was open; a logged-out tab is still mounted; a new deploy changed the auth provider config; local dev without dev-mode env flag. Unlike mutations using `requireUserId`, this one has no dev fallback, so dev environments need a real Clerk session.","solutions":["Ensure the client is authenticated via Clerk before calling — check `useAuth().isSignedIn` and gate the follow button on it.","On `err.data.kind === \"UNAUTHENTICATED\"`, redirect to sign-in and resume the follow action after re-auth.","For local dev without Clerk, set `CONVEX_IS_DEV=true` — note this mutation bypasses `requireUserId` so dev fallback still won't apply; use a real session.","Refresh the auth token (Clerk auto-refreshes; a stale token usually resolves on reload)."],"exampleFix":"// before\nconst onFollow = () => convex.mutation(api.followedCountries.followCountry, { country });\n\n// after — gate on auth state and handle the unauthenticated branch\nconst { isSignedIn } = useAuth();\nconst onFollow = async () => {\n  if (!isSignedIn) { navigate(\"/sign-in?redirect=\" + location.pathname); return; }\n  try {\n    await convex.mutation(api.followedCountries.followCountry, { country });\n  } catch (err) {\n    if (err.data?.kind === \"UNAUTHENTICATED\") {\n      navigate(\"/sign-in?redirect=\" + location.pathname);\n    } else throw err;\n  }\n};","handlingStrategy":"try-catch","validationCode":"import { useAuth } from \"@clerk/clerk-react\";\n// before calling:\nconst { isSignedIn } = useAuth();\nif (!isSignedIn) { navigate(\"/sign-in\"); return; }","typeGuard":"// Auth state comes from Clerk's hook; no local type guard applies.\n// Treat isSignedIn === true as the gate.","tryCatchPattern":"try {\n  await convex.mutation(api.followedCountries.followCountry, { country });\n} catch (err) {\n  if (err.data?.kind === \"UNAUTHENTICATED\") navigate(\"/sign-in?redirect=\" + location.pathname);\n  else throw err;\n}","preventionTips":["Gate the follow button on `useAuth().isSignedIn`.","Handle session-expiry by redirecting to sign-in with a return-to path.","This mutation has NO dev-mode fallback — use a real Clerk session in dev.","Refresh followed-countries after re-auth before retrying."],"tags":["auth","convex","clerk","follow"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}