koala73/worldmonitor · error · ConvexError
FREE_EMAIL_NOT_ALLOWED
FREE_EMAIL_NOT_ALLOWED
Error message
Please use a corporate email address.
What it means
Thrown by `submit` after the email passes shape validation but `isCorporateDomain(email)` returns false. A domain is corporate only if it has a dot (TLD), is NOT in the `FREE_EMAIL_DOMAINS` set (gmail/yahoo/outlook/icloud/proton/etc.), and passes `mailchecker.isValid()` (rejecting disposable/temporary providers like mailinator, 10minutemail). Carries object data `{ kind: "FREE_EMAIL_NOT_ALLOWED", message: "..." }`, so the client branches on `err.data.kind`. This is an intentional B2B gate — the contact form is for enterprise/business inquiries, not consumer mail.
Source
Thrown at convex/contactMessages.ts:72
source: v.string(),
},
handler: async (ctx, args) => {
// Length / shape validation. Reject obviously-bogus input before
// it reaches the table — also a defence against prompt-injection
// payloads enormous enough to trip downstream LLM cost.
const name = clip(args.name, MAX_NAME);
const email = clip(args.email, MAX_EMAIL);
const organization = clip(args.organization, MAX_ORG);
const phone = clip(args.phone, MAX_PHONE);
const message = clip(args.message, MAX_MESSAGE, { preserveNewlines: true });
const source = clip(args.source, MAX_SOURCE) ?? "unknown";
if (!name) throw new ConvexError("Name is required");
if (!email || !EMAIL_RE.test(email)) {
throw new ConvexError("Valid email is required");
}
if (!isCorporateDomain(email)) {
throw new ConvexError({
kind: "FREE_EMAIL_NOT_ALLOWED",
message: "Please use a corporate email address.",
});
}
const normalizedEmail = email.toLowerCase();
// Throttle: cap recent submissions per email. Index lookup keeps this O(matches),
// which the limit caps at PER_EMAIL_LIMIT + 1.
const windowStart = Date.now() - PER_EMAIL_WINDOW_MS;
const recent = await ctx.db
.query("contactMessages")
.withIndex("by_normalized_email_received", (q) =>
q.eq("normalizedEmail", normalizedEmail).gte("receivedAt", windowStart),
)
.take(PER_EMAIL_LIMIT + 1);
if (recent.length >= PER_EMAIL_LIMIT) {View on GitHub (pinned to ffec79ac33)
Solutions
- Use a corporate/work email address with a non-free domain (e.g. `you@company.com`).
- If the user's company runs on Google Workspace/Microsoft 365, use the custom domain address, not the @gmail/@outlook one.
- Client-side: call the same `isCorporateDomain` check (or a mirror of `FREE_EMAIL_DOMAINS`) before submit to give instant feedback.
- If the block is wrong for a legitimate domain, verify it isn't in `mailchecker`'s disposable list — `mailchecker.isValid("user@domain")` must return true.
Example fix
// before
await convex.mutation(api.contactMessages.submit, { name, email: "jane.doe@gmail.com", source });
// -> ConvexError { kind: "FREE_EMAIL_NOT_ALLOWED" }
// after — use the corporate address
await convex.mutation(api.contactMessages.submit, { name, email: "jane.doe@acme.com", source }); Defensive patterns
Strategy: validation
Validate before calling
const FREE = new Set(["gmail.com","googlemail.com","yahoo.com","outlook.com","hotmail.com","icloud.com","protonmail.com","proton.me","aol.com",/* ... full server list */]);
function isCorporate(email: string): boolean {
const at = email.lastIndexOf("@");
if (at < 0) return false;
const domain = email.slice(at + 1).toLowerCase();
return domain.includes(".") && !FREE.has(domain);
}
if (!isCorporate(email)) { showFieldError("Use a corporate email."); return; } Type guard
function isCorporateEmail(email: string): boolean {
const domain = email.split("@")[1]?.toLowerCase();
return !!domain && domain.includes(".") && !FREE_EMAIL_DOMAINS.has(domain);
} Try / catch
try {
await convex.mutation(api.contactMessages.submit, { ... });
} catch (err) {
if (err.data?.kind === "FREE_EMAIL_NOT_ALLOWED") setFieldError("email", "Please use a corporate email.");
else throw err;
} Prevention
- Mirror the server's FREE_EMAIL_DOMAINS set client-side.
- Validate the corporate-domain rule before submit.
- For Google Workspace users, prompt for the custom-domain address, not @gmail.
- Run mailchecker locally to also catch disposable domains.
When it happens
Trigger: Calling `submit` with a free-provider email (`user@gmail.com`, `user@outlook.com`, `user@yahoo.co.uk`, `user@proton.me`, `user@icloud.com`); a disposable/temporary email (`user@mailinator.com`, `user@10minutemail.com` — caught by mailchecker); or a malformed/bare-hostname domain with no TLD (`user@intranet`). The `FREE_EMAIL_DOMAINS` set and mailchecker list are the two rejection sources.
Common situations: A user fills the enterprise contact form with their personal Gmail; QA/testing with a throwaway inbox; a prospect whose company uses Google Workspace but types their personal address instead of the corporate one (note: a custom domain on Google Workspace like `user@acme.com` passes because the domain isn't in the free list). Locale-specific providers (qq.com, yandex.ru, web.de, orange.fr) are also blocked.
Related errors
- Valid email is required
- email required for email channel
- OWNER_EMAIL_UNAVAILABLE
- OWNER_DOMAIN_NOT_CORPORATE
- INVITEE_DOMAIN_NOT_CORPORATE
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/1710619a6e3da0a3.
Report an issue: GitHub.