koala73/worldmonitor · error · ConvexError
OWNER_NOT_BUSINESS
OWNER_NOT_BUSINESS
Error message
OWNER_NOT_BUSINESS
What it means
Thrown by `inviteSeats` when `getCoveringBusinessSubscription` returns null — the caller has no active Business-tier subscription at the current time, so they are not authorized to invite teammates. Object-typed ConvexError (`{ kind: "OWNER_NOT_BUSINESS" }`).
Source
Thrown at convex/payments/businessSeats.ts:132
/**
* Owner invites up to 4 same-domain teammates. Pending invites count against
* the cap; each gets a single-use HMAC token emailed via Resend.
*/
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(View on GitHub (pinned to ffec79ac33)
Solutions
- Upgrade to (or reactivate) a Business subscription before inviting seats.
- Gate the invite UI on an active Business entitlement check so the option is hidden for ineligible users.
- If the subscription should be active, verify the entitlement recompute picked up the latest Business sub.
Example fix
// before
await inviteSeats({ emails: [...] });
// after
const entitlement = await getMyEntitlement();
if (entitlement.plan !== "business") { routeToUpgrade(); return; }
await inviteSeats({ emails: [...] }); Defensive patterns
Strategy: validation
Validate before calling
const entitlement = await getMyEntitlement();
if (entitlement.plan !== "business" || !entitlement.active) {
routeToUpgrade();
return;
}
await inviteSeats({ emails }); Type guard
function isBusinessEntitlement(e: { plan: string; active: boolean }): boolean {
return e.plan === "business" && e.active;
} Prevention
- Gate the seat-invite UI on an active Business entitlement.
- Re-check entitlement after a subscription change before allowing invites.
When it happens
Trigger: A Pro (non-Business) or free user opens the seat-invite UI and calls `inviteSeats`. Also fires for a former Business subscriber whose subscription expired or was cancelled.
Common situations: User downgraded from Business to Pro; Business subscription lapsed/expired; the UI failed to gate the invite panel behind an active Business entitlement; grace period ended.
Related errors
- OWNER_EMAIL_UNAVAILABLE
- OWNER_DOMAIN_NOT_CORPORATE
- NO_EMAILS_PROVIDED
- TOO_MANY_EMAILS
- CANNOT_INVITE_SELF
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/a5ffa123b08986e0.
Report an issue: GitHub.