koala73/worldmonitor · error · ConvexError

INVITEE_DOMAIN_NOT_CORPORATE

INVITEE_DOMAIN_NOT_CORPORATE

Error message

INVITEE_DOMAIN_NOT_CORPORATE

What it means

Thrown by `inviteSeats` per-email when an invitee's email is not on a corporate domain (`isCorporateDomain` returns false): missing domain, no dot, a free/consumer provider, or disposable. Business seats may only go to real company addresses. Object-typed ConvexError (`{ kind: "INVITEE_DOMAIN_NOT_CORPORATE" }`).

Source

Thrown at convex/payments/businessSeats.ts:185

    const existingByEmail = new Map(
      existingGrants.map((g) => [g.inviteeEmail, g]),
    );

    // Separate new invites from duplicates before the cap check so a duplicate
    // re-invite is idempotent even when the cap is full.
    const newEmails: string[] = [];
    const results: Array<{
      email: string;
      grantId: string;
      status: "created" | "already_pending" | "already_accepted";
    }> = [];

    for (const email of emails) {
      if (email === normalizedOwnerEmail) {
        throw new ConvexError({ kind: "CANNOT_INVITE_SELF" });
      }
      if (!isCorporateDomain(email)) {
        throw new ConvexError({ kind: "INVITEE_DOMAIN_NOT_CORPORATE" });
      }
      if (!sameDomain(ownerEmail, email)) {
        throw new ConvexError({ kind: "INVITEE_DOMAIN_MISMATCH" });
      }

      const existing = existingByEmail.get(email);
      if (existing) {
        if (existing.status === "accepted") {
          results.push({ email, grantId: existing._id, status: "already_accepted" });
          continue;
        }
        if (existing.status === "pending" && existing.expiresAt > now) {
          results.push({ email, grantId: existing._id, status: "already_pending" });
          continue;
        }
      }
      newEmails.push(email);
    }

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Replace the invitee address with their corporate-domain email (same company as the owner).
  2. Validate each invitee with `isCorporateDomain` client-side before submit.
  3. Reject disposable/free addresses in the form with a clear message.

Example fix

// before
await inviteSeats({ emails: ["teammate@gmail.com"] });

// after
await inviteSeats({ emails: ["teammate@acme.com"] });
Defensive patterns

Strategy: validation

Validate before calling

for (const e of emails) {
  if (!isCorporateDomain(e)) {
    showToast(`${e} is not a corporate email.`);
    return;
  }
}
await inviteSeats({ emails });

Type guard

function isCorporateDomain(email: string): boolean {
  const domain = (email.split("@").pop() ?? "").toLowerCase();
  return domain.includes(".") && !FREE_PROVIDERS.has(domain);
}

Prevention

When it happens

Trigger: The invite list contains a personal/free address (gmail, outlook, yahoo) or a disposable/temporary email, or a malformed address with no dotted domain.

Common situations: Owner typed a teammate's personal Gmail instead of their work address; a throwaway email; a typo producing an invalid domain; teammate uses a consumer provider.

Related errors


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