koala73/worldmonitor · error · ConvexError
OWNER_DOMAIN_NOT_CORPORATE
OWNER_DOMAIN_NOT_CORPORATE
Error message
OWNER_DOMAIN_NOT_CORPORATE
What it means
Thrown by `inviteSeats` when the owner's email domain is not a 'corporate' domain per `extractDomain`/`isCorporateDomain`: the domain is missing, has no dot (bare hostname), is a free/consumer provider (gmail, outlook, etc.), or is disposable/temporary. Business seat invites require the owner to be on a real company domain. Object-typed ConvexError (`{ kind: "OWNER_DOMAIN_NOT_CORPORATE" }`).
Source
Thrown at convex/payments/businessSeats.ts:137
export const inviteSeats = mutation({
args: { emails: v.array(v.string()) },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const identity = await resolveUserIdentity(ctx);
const ownerEmail = identity?.email?.trim();
if (!ownerEmail) {
throw new ConvexError({ kind: "OWNER_EMAIL_UNAVAILABLE" });
}
const now = Date.now();
const businessSub = await getCoveringBusinessSubscription(ctx, userId, now);
if (!businessSub) {
throw new ConvexError({ kind: "OWNER_NOT_BUSINESS" });
}
const ownerDomain = extractDomain(ownerEmail);
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" });
}View on GitHub (pinned to ffec79ac33)
Solutions
- Have the owner switch their account email to a genuine corporate domain (with a dot, not free/disposable).
- If the company domain is wrongly flagged, verify it isn't on the disposable/free list and file a correction.
- Gate the invite UI on `isCorporateDomain(ownerEmail)` before letting the owner proceed.
Example fix
// before
await inviteSeats({ emails: [...] }); // owner uses user@gmail.com
// after
// owner updates account email to user@acme.com first
await inviteSeats({ emails: [...] }); Defensive patterns
Strategy: validation
Validate before calling
const ownerEmail = user.primaryEmailAddress!.emailAddress;
if (!isCorporateDomain(ownerEmail)) {
showToast("Use a corporate email address to invite teammates.");
return;
}
await inviteSeats({ emails }); Type guard
function isCorporateDomain(email: string): boolean {
const domain = email.split("@").pop() ?? "";
return domain.includes(".") && !FREE_PROVIDERS.has(domain.toLowerCase());
} Prevention
- Encourage owners to sign up with a real company domain.
- Block free/disposable provider addresses at sign-up for Business accounts.
When it happens
Trigger: A Business owner whose email is on a free provider (e.g. `@gmail.com`), a disposable domain, or a malformed/bare-hostname address calls `inviteSeats`.
Common situations: Owner signed up with a personal Gmail/Outlook/Yahoo address; a disposable-email address; a misconfigured email like `user@localhost`; the domain resolver flagged the company domain as disposable via the mailchecker list.
Related errors
- INVITEE_DOMAIN_NOT_CORPORATE
- INVITEE_DOMAIN_MISMATCH
- OWNER_EMAIL_UNAVAILABLE
- OWNER_NOT_BUSINESS
- NO_EMAILS_PROVIDED
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/834aae772b7c57f5.
Report an issue: GitHub.