koala73/worldmonitor · error · ConvexError

EMAIL_OWNERSHIP_REQUIRED

EMAIL_OWNERSHIP_REQUIRED

Error message

Connect your verified account email to receive notifications.

What it means

requireVerifiedAccountEmail throws EMAIL_OWNERSHIP_REQUIRED when the email a caller wants notifications sent to cannot be proven to belong to the authenticated account. Proof comes only from the authenticated identity or the server's Clerk lookup — never from client-supplied strings. The requested email must exactly match (case-insensitively) the verified email on the account; otherwise the function refuses to return any recipient.

Solutions

  1. Send the email exactly as it appears on the Clerk account, or omit it so the server uses lookupVerifiedAccountEmail.
  2. Have the user verify the address in Clerk (verification email) before using it as a notification recipient.
  3. Check case/whitespace: the comparison trims and lowercases, but any other character difference (e.g. plus-alias) fails.
  4. Ensure CLERK_SECRET_KEY is set so lookupVerifiedAccountEmail can resolve the verified email server-side.

Example fix

// before
await api.notifications.addRecipient({ email: formData.email });
// after
// use the verified email from the auth identity, or omit to let the server resolve it
await api.notifications.addRecipient({ email: clerkUser.primaryEmailAddress.emailAddress });
Defensive patterns

Strategy: try-catch

Validate before calling

const requested = (email ?? "").trim().toLowerCase();
const verified = (clerkUser.primaryEmailAddress?.emailAddress ?? "").trim().toLowerCase();
if (!verified || requested !== verified) throw new Error("recipient must be the verified account email");

Try / catch

try {
  await addRecipient({ email });
} catch (e) {
  if (isConvexError(e) && e.data?.code === "EMAIL_OWNERSHIP_REQUIRED") {
    promptUserToVerifyEmail();
  }
}

Prevention

When it happens

Trigger: Calling a mutation/query (verifiedAccountEmail, recipient, email paths) that passes a `requested` email which is undefined, empty after trim, or differs from the Clerk-verified email (different case is tolerated, but any character difference is not). Typical calls: registering a notification recipient with a personal email while the Clerk account has a different verified address, or passing no email at all when the identity has none.

Common situations: User signed up with Google SSO so no email was passed through the identity; developer forwards a user-typed email from a form instead of the verified one; user changed their Clerk primary email but the client caches the old one; email has stray whitespace or a plus-address variant that fails the strict lowercase comparison.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/1d8a82175514fa38. Report an issue: GitHub.

Appendix: source

Thrown at convex/lib/notificationEmail.ts:7

import { ConvexError } from "convex/values";

/** Only the authenticated identity or the server's Clerk lookup supplies proof. */
export function requireVerifiedAccountEmail(requested: string | undefined, verifiedEmail: string | undefined): string {
  const email = verifiedEmail?.trim();
  if (!email || typeof requested !== "string" || requested.trim().toLowerCase() !== email.toLowerCase()) {
    throw new ConvexError({ code: "EMAIL_OWNERSHIP_REQUIRED", message: "Connect your verified account email to receive notifications." });
  }
  return email;
}

export async function lookupVerifiedAccountEmail(userId: string): Promise<string | undefined> {
  const secret = process.env.CLERK_SECRET_KEY;
  if (!secret) throw new Error("EMAIL_VERIFICATION_UNAVAILABLE");
  const response = await fetch(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}`, {
    headers: { Authorization: `Bearer ${secret}`, "User-Agent": "worldmonitor-convex/1.0" },
    signal: AbortSignal.timeout(5_000),
  });
  if (!response.ok) throw new Error("EMAIL_VERIFICATION_UNAVAILABLE");
  const user = await response.json() as {
    id?: string;
    primary_email_address_id?: string;
    email_addresses?: Array<{ id: string; email_address: string; verification?: { status?: string } }>;
  };
  if (user.id !== userId) throw new Error("EMAIL_VERIFICATION_UNAVAILABLE");

View on GitHub (pinned to 7d06c8633d)