koala73/worldmonitor · error · ConvexError
OWNER_EMAIL_UNAVAILABLE
OWNER_EMAIL_UNAVAILABLE
Error message
OWNER_EMAIL_UNAVAILABLE
What it means
Thrown by the `inviteSeats` mutation in convex/payments/businessSeats.ts when the authenticated owner's identity has no resolvable email address (`resolveUserIdentity` returned no trimmed email). The owner email is required to send Resend invite emails and to derive the corporate domain, so the call aborts early. Object-typed ConvexError (`{ kind: "OWNER_EMAIL_UNAVAILABLE" }`).
Source
Thrown at convex/payments/businessSeats.ts:126
return grants.filter((g) => {
if (g.status === "accepted") return true;
if (g.status === "pending" && g.expiresAt > at) return true;
return false;
}).length;
}
/**
* Owner invites up to 4 same-domain teammates. Pending invites count against
* the cap; each gets a single-use HMAC token emailed via Resend.
*/
export const inviteSeats = mutation({
args: { emails: v.array(v.string()) },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const identity = await resolveUserIdentity(ctx);
const ownerEmail = identity?.email?.trim();
if (!ownerEmail) {
throw new ConvexError({ kind: "OWNER_EMAIL_UNAVAILABLE" });
}
const now = Date.now();
const businessSub = await getCoveringBusinessSubscription(ctx, userId, now);
if (!businessSub) {
throw new ConvexError({ kind: "OWNER_NOT_BUSINESS" });
}
const ownerDomain = extractDomain(ownerEmail);
if (!ownerDomain || !isCorporateDomain(ownerEmail)) {
throw new ConvexError({ kind: "OWNER_DOMAIN_NOT_CORPORATE" });
}
// Serialize concurrent inviteSeats / removeSeat calls for this Business
// subscription so the cap check below cannot be bypassed by a race.
await touchBusinessSeatLock(ctx, businessSub.dodoSubscriptionId, now);
const normalizedOwnerEmail = ownerEmail.toLowerCase();View on GitHub (pinned to ffec79ac33)
Solutions
- Have the owner add/verify an email address in their account settings, then retry.
- Ensure the Clerk OAuth scopes include email (`email` scope) so the identity populates it.
- If email is present but unverified, require verification before enabling seat invites.
Example fix
// before
await inviteSeats({ emails: ["teammate@corp.com"] });
// after
if (!user.primaryEmailAddress) { promptAddEmail(); return; }
await inviteSeats({ emails: ["teammate@corp.com"] }); Defensive patterns
Strategy: validation
Validate before calling
const ownerEmail = user.primaryEmailAddress?.emailAddress?.trim();
if (!ownerEmail) {
promptAddEmail();
return;
}
await inviteSeats({ emails }); Type guard
function hasOwnerEmail(u: { primaryEmailAddress?: { emailAddress?: string } | null }): u is { primaryEmailAddress: { emailAddress: string } } {
return !!u.primaryEmailAddress && !!u.primaryEmailAddress.emailAddress?.trim();
} Prevention
- Require a verified primary email on the owner account before enabling seat invites.
- Request the `email` OAuth scope on social sign-in providers.
When it happens
Trigger: An authenticated Business owner calls `inviteSeats` but their Clerk identity has no email (e.g. signed in via OAuth provider that didn't expose email, or email is unverified/private).
Common situations: GitHub OAuth sign-in with a private email; a social login provider that omits the email scope; the Clerk user record lost its email after a merge; SSO account with email pending verification.
Related errors
- OWNER_DOMAIN_NOT_CORPORATE
- INVITEE_DOMAIN_NOT_CORPORATE
- INVITEE_DOMAIN_MISMATCH
- OWNER_NOT_BUSINESS
- NO_EMAILS_PROVIDED
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/3594e6ecb008ccf5.
Report an issue: GitHub.