koala73/worldmonitor · error · ConvexError

TOO_MANY_EMAILS

TOO_MANY_EMAILS

Error message

TOO_MANY_EMAILS

What it means

Structured ConvexError thrown by inviteSeats when the number of invite emails exceeds the per-call maximum. The mutation caps how many addresses may be submitted at once; larger batches are rejected before creating any grants.

Solutions

  1. Split the invite list into smaller batches within the per-call limit
  2. Trim invalid or already-invited entries client-side before submitting
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/payments/businessSeats.ts:157 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/2eb1bc88ffa82076. Report an issue: GitHub.

Appendix: source

Thrown at convex/payments/businessSeats.ts:156

    // 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 7d06c8633d)