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
- Reduce the list to at most 4 distinct emails per call.
- Enforce the `MAX_SEATS = 4` limit in the UI before submit.
- 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
- Enforce the 4-seat cap in the UI.
- Remember pending invites also count toward the cap; revoke unused ones to free slots.
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
- OWNER_EMAIL_UNAVAILABLE
- OWNER_NOT_BUSINESS
- OWNER_DOMAIN_NOT_CORPORATE
- NO_EMAILS_PROVIDED
- CANNOT_INVITE_SELF
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/2eb1bc88ffa82076.
Report an issue: GitHub.