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

  1. Ensure the signed-in user's Clerk profile has a primary email address
  2. Re-authenticate so a fresh identity token with email claims is available
  3. 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)