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
- Gate the channel settings UI on `useAuth().isSignedIn`.
- On `err.data === "UNAUTHENTICATED"`, redirect to sign-in and resume the channel setup after re-auth.
- 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
- Gate the channel settings UI on `isSignedIn`.
- This module uses plain-string ConvexError — branch on err.data === "UNAUTHENTICATED".
- Re-fetch channels after re-auth before retrying.
- Redirect to sign-in with a return path.
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
- 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/0d004ebd421b5293.
Report an issue: GitHub.