koala73/worldmonitor · error · ConvexError

UNAUTHENTICATED

UNAUTHENTICATED

Error message

UNAUTHENTICATED

What it means

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.

Source

Thrown at convex/followedCountries.ts:343

 *   Tier 2 — denormalized user-meta count (Codex round-3 P0): under the
 *   shard lock, we safely lazy-create the per-user `followedCountriesUserMeta`
 *   row (kept additionally for the O(1) cap-check denominator and as the
 *   parity invariant `count === COUNT(followedCountries WHERE userId=X)`).
 *
 * Without Tier 1, two parallel first-ever mutations could both read
 * `meta=undefined`, both INSERT, and produce duplicate meta rows that
 * break the `.unique()` read AND re-open the cap-bypass window. With
 * Tier 1 in place, the brand-new-user race is closed deterministically.
 *
 * Errors are typed `ConvexError({kind, ...})` with object data so callers
 * can branch on `err.data.kind` (memory:
 * `convex-error-string-data-strips-errordata-on-wire`).
 */
export const followCountry = mutation({
  args: { country: v.string() },
  handler: async (ctx, args): Promise<FollowMutationResult> => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError({ kind: "UNAUTHENTICATED" });
    const userId = identity.subject;

    if (!isValidIso2(args.country)) {
      throw new ConvexError({
        kind: "INVALID_COUNTRY",
        country: args.country,
      });
    }

    // Tier-1 lock: pre-seeded shard row. Read at top, patch at end.
    const shard = await readShardOrThrow(ctx, userId);

    // Tier-2 read: per-user denormalized count (lazy-created, but safe
    // under the shard lock above).
    const { meta, count: currentCount } = await readUserMeta(ctx, userId);

    const existingRow = await ctx.db
      .query("followedCountries")

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Ensure the client is authenticated via Clerk before calling — check `useAuth().isSignedIn` and gate the follow button on it.
  2. On `err.data.kind === "UNAUTHENTICATED"`, redirect to sign-in and resume the follow action after re-auth.
  3. 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.
  4. Refresh the auth token (Clerk auto-refreshes; a stale token usually resolves on reload).

Example fix

// before
const onFollow = () => convex.mutation(api.followedCountries.followCountry, { country });

// after — gate on auth state and handle the unauthenticated branch
const { isSignedIn } = useAuth();
const onFollow = async () => {
  if (!isSignedIn) { navigate("/sign-in?redirect=" + location.pathname); return; }
  try {
    await convex.mutation(api.followedCountries.followCountry, { country });
  } catch (err) {
    if (err.data?.kind === "UNAUTHENTICATED") {
      navigate("/sign-in?redirect=" + location.pathname);
    } else throw err;
  }
};
Defensive patterns

Strategy: try-catch

Validate before calling

import { useAuth } from "@clerk/clerk-react";
// before calling:
const { isSignedIn } = useAuth();
if (!isSignedIn) { navigate("/sign-in"); return; }

Type guard

// Auth state comes from Clerk's hook; no local type guard applies.
// Treat isSignedIn === true as the gate.

Try / catch

try {
  await convex.mutation(api.followedCountries.followCountry, { country });
} catch (err) {
  if (err.data?.kind === "UNAUTHENTICATED") navigate("/sign-in?redirect=" + location.pathname);
  else throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/56a542440e4af645. Report an issue: GitHub.