koala73/worldmonitor · error · ConvexError

TOO_MANY_EMAILS

TOO_MANY_EMAILS

Error message

TOO_MANY_EMAILS

What it means

Thrown by `inviteSeats` when the normalized (deduped, non-blank) emails count exceeds `MAX_SEATS` (4). Because the cap is checked AFTER dedup, genuine duplicates do not consume slots, but distinct addresses beyond 4 are rejected. Object-typed ConvexError (`{ kind: "TOO_MANY_EMAILS" }`).

Source

Thrown at convex/payments/businessSeats.ts:157

    // 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();
    // Dedupe: the existingByEmail check below only guards against emails
    // that already had a grant BEFORE this call — a duplicate within the
    // SAME args.emails array would otherwise slip past it (neither
    // occurrence is in existingGrants yet) and create two grant rows for
    // one invitee.
    const emails = Array.from(
      new Set(args.emails.map((e) => e.trim().toLowerCase()).filter((e) => e.length > 0)),
    );
    if (emails.length === 0) {
      throw new ConvexError({ kind: "NO_EMAILS_PROVIDED" });
    }
    if (emails.length > MAX_SEATS) {
      throw new ConvexError({ kind: "TOO_MANY_EMAILS" });
    }

    // Check existing grants for duplicates (active or pending).
    const existingGrants = await ctx.db
      .query("businessProGrants")
      .withIndex("by_businessSubscriptionId", (q) =>
        q.eq("businessSubscriptionId", businessSub.dodoSubscriptionId),
      )
      .collect();
    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;

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Reduce the list to at most 4 distinct emails per call.
  2. Enforce the `MAX_SEATS = 4` limit in the UI before submit.
  3. Note pending invites also count against the cap; revoke unused pending invites to free slots if needed.

Example fix

// before
await inviteSeats({ emails: fiveEmails });

// after
const MAX_SEATS = 4;
const emails = uniq(emailsInput).slice(0, MAX_SEATS);
if (emailsInput.length > MAX_SEATS) showToast("Max 4 seats per invite.");
await inviteSeats({ emails });
Defensive patterns

Strategy: validation

Validate before calling

const MAX_SEATS = 4;
const emails = Array.from(new Set(rawEmails.map(e => e.trim().toLowerCase()).filter(Boolean)));
if (emails.length > MAX_SEATS) {
  showToast(`Max ${MAX_SEATS} seats per invite.`);
  return;
}
await inviteSeats({ emails });

Prevention

When it happens

Trigger: Calling `inviteSeats` with more than 4 distinct valid emails, e.g. 5+ teammates in one call.

Common situations: Owner tries to invite the whole team at once; pasting a list of 5+ addresses; UI didn't enforce the 4-seat maximum client-side.

Related errors


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