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
- Send the email exactly as it appears on the Clerk account, or omit it so the server uses lookupVerifiedAccountEmail.
- Have the user verify the address in Clerk (verification email) before using it as a notification recipient.
- Check case/whitespace: the comparison trims and lowercases, but any other character difference (e.g. plus-alias) fails.
- 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
- Always source the recipient email from the Clerk identity, never free-form user input.
- Prompt users to verify their email in Clerk before enabling email notifications.
- Show the verified email in the UI instead of an editable field.
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
- UNAUTHENTICATED
- Authentication unavailable while loading MCP clients. Try ag
- Authentication unavailable while loading Business Pro seats.
- Account changed while creating the embed key. Try again.
- Authentication unavailable while loading embed keys. Try aga
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)