koala73/worldmonitor · error · ConvexError

UNAUTHENTICATED

UNAUTHENTICATED

Error message

UNAUTHENTICATED

What it means

Thrown by the `setChannel` mutation when `ctx.auth.getUserIdentity()` is falsy. This is the auth gate before the Pro-entitlement check and channel-specific validation. Plain-string ConvexError; `err.data === "UNAUTHENTICATED"`. Note this module uses string throws (branch on `err.data === "UNAUTHENTICATED"`), unlike followedCountries which uses `{kind}` objects.

Source

Thrown at convex/notificationChannels.ts:479

    if (!identity) return [];
    return await ctx.db
      .query("notificationChannels")
      .withIndex("by_user", (q) => q.eq("userId", identity.subject))
      .collect();
  },
});

export const setChannel = mutation({
  args: {
    channelType: channelTypeValidator,
    chatId: v.optional(v.string()),
    webhookEnvelope: v.optional(v.string()),
    email: v.optional(v.string()),
    webhookLabel: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("UNAUTHENTICATED");
    const userId = identity.subject;
    await assertProEntitlement(ctx, userId);

    const existing = await ctx.db
      .query("notificationChannels")
      .withIndex("by_user_channel", (q) =>
        q.eq("userId", userId).eq("channelType", args.channelType),
      )
      .unique();

    const now = Date.now();

    if (args.channelType === "telegram") {
      if (!args.chatId) throw new ConvexError("chatId required for telegram channel");
      const doc = { userId, channelType: "telegram" as const, chatId: args.chatId, verified: true, linkedAt: now };
      if (existing) {
        await ctx.db.replace(existing._id, doc);
      } else {

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Gate the channel settings UI on `useAuth().isSignedIn`.
  2. On `err.data === "UNAUTHENTICATED"`, redirect to sign-in and resume the channel setup after re-auth.
  3. Re-fetch channel state after re-auth before retrying (another session may have changed it).

Example fix

// before
const save = () => convex.mutation(api.notificationChannels.setChannel, { channelType, chatId });

// after
const { isSignedIn } = useAuth();
const save = async () => {
  if (!isSignedIn) { navigate("/sign-in"); return; }
  try {
    await convex.mutation(api.notificationChannels.setChannel, { channelType, chatId });
  } catch (err) {
    if (err.data === "UNAUTHENTICATED") navigate("/sign-in");
    else throw err;
  }
};
Defensive patterns

Strategy: try-catch

Validate before calling

const { isSignedIn } = useAuth();
if (!isSignedIn) { navigate("/sign-in"); return; }

Type guard

// Auth gate via Clerk hook; no local type guard.

Try / catch

try {
  await convex.mutation(api.notificationChannels.setChannel, { ... });
} catch (err) {
  if (err.data === "UNAUTHENTICATED") navigate("/sign-in");
  else throw err;
}

Prevention

When it happens

Trigger: Calling `api.notificationChannels.setChannel` without an authenticated Clerk session; the session expired while the notification settings panel was open; calling from a context with no forwarded auth token.

Common situations: Session expiry on an open settings tab; a logged-out user reaches the panel via deep link; the auth token isn't attached to the Convex client after a provider config change.

Understand the failure class

Related errors


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