koala73/worldmonitor · error · ConvexError
NO_EMAILS_PROVIDED
NO_EMAILS_PROVIDED
Error message
NO_EMAILS_PROVIDED
What it means
Structured ConvexError thrown by inviteSeats when the normalized invite email list is empty after validation/deduplication. The mutation requires at least one invitee email; an empty submission is rejected before the seat-cap lock is used.
Solutions
- Submit at least one valid email address in the emails array
- Guard the UI to disable the invite button when the input list is empty
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at convex/payments/businessSeats.ts:154 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/4dbdeafd392e4250.
Report an issue: GitHub.
Appendix: source
Thrown at convex/payments/businessSeats.ts:153
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();
// 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.View on GitHub (pinned to 7d06c8633d)