koala73/worldmonitor · error · ConvexError

SEAT_CAP_REACHED

SEAT_CAP_REACHED

Error message

SEAT_CAP_REACHED

What it means

Structured ConvexError thrown by inviteSeats when the count of currently active or pending seat grants plus the new (non-duplicate) invites would exceed MAX_SEATS (4). The check runs after the seat lock serializes concurrent calls, so the cap cannot be bypassed by racing invitations.

Solutions

  1. Remove or revoke existing seats before inviting replacements
  2. Drop pending invites that are no longer needed to free cap space
  3. Surface the remaining-seat count in the UI before submission
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/payments/businessSeats.ts:211 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/e9dbf4078ecf6fb3. Report an issue: GitHub.

Appendix: source

Thrown at convex/payments/businessSeats.ts:207

        if (existing.status === "accepted") {
          results.push({ email, grantId: existing._id, status: "already_accepted" });
          continue;
        }
        if (existing.status === "pending" && existing.expiresAt > now) {
          results.push({ email, grantId: existing._id, status: "already_pending" });
          continue;
        }
      }
      newEmails.push(email);
    }

    const currentCount = await countActiveOrPendingGrants(
      ctx,
      businessSub.dodoSubscriptionId,
      now,
    );
    if (currentCount + newEmails.length > MAX_SEATS) {
      throw new ConvexError({ kind: "SEAT_CAP_REACHED" });
    }

    for (const email of newEmails) {
      const grantId = await ctx.db.insert("businessProGrants", {
        businessSubscriptionId: businessSub.dodoSubscriptionId,
        ownerUserId: userId,
        inviteeEmail: email,
        domain: ownerDomain,
        status: "pending",
        createdAt: now,
        expiresAt: now + INVITE_TTL_MS,
      });

      const token = await signBusinessInviteToken(grantId);
      await ctx.scheduler.runAfter(
        0,
        internal.payments.businessSeats.sendBusinessInviteEmail,
        { inviteeEmail: email, ownerEmail, grantId, token },

View on GitHub (pinned to 7d06c8633d)