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

  1. Have the owner add/verify an email address in their account settings, then retry.
  2. Ensure the Clerk OAuth scopes include email (`email` scope) so the identity populates it.
  3. 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

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


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/3594e6ecb008ccf5. Report an issue: GitHub.