koala73/worldmonitor · error · ConvexError
OWNER_EMAIL_UNAVAILABLE
OWNER_EMAIL_UNAVAILABLE
Error message
OWNER_EMAIL_UNAVAILABLE
What it means
Structured ConvexError thrown by the inviteSeats mutation when the seat-owning user's Clerk identity has no email address available. Business seat invites are keyed to the owner's corporate email domain, so without a resolvable owner email the invite flow cannot proceed past its first guard.
Solutions
- Ensure the signed-in user's Clerk profile has a primary email address
- Re-authenticate so a fresh identity token with email claims is available
- If email is deliberately withheld, that account cannot own Business seat invites by design
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at convex/payments/businessSeats.ts:126 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21).
Data as JSON: /api/errors/3594e6ecb008ccf5.
Report an issue: GitHub.
Appendix: source
Thrown at convex/payments/businessSeats.ts:125
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 teammates at any corporate email domain. 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 7d06c8633d)