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
- 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).
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
- 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.
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/56a542440e4af645.
Report an issue: GitHub.