koala73/worldmonitor · error · ConvexError
GRANT_NOT_FOUND
GRANT_NOT_FOUND
Error message
GRANT_NOT_FOUND
What it means
Thrown by the `removeSeat` mutation when `ctx.db.get(args.grantId)` returns null — the supplied ID does not resolve to a `businessProGrants` document. The grant may never have existed, may have been deleted, or the ID is not a valid Convex document ID for that table.
Source
Thrown at convex/payments/businessSeats.ts:421
createdAt: g.createdAt,
acceptedAt: g.acceptedAt ?? null,
expiresAt: g.expiresAt,
})),
};
},
});
/**
* Owner-only removal of a single seat. Revokes the grant and recomputes the
* invitee's entitlement.
*/
export const removeSeat = mutation({
args: { grantId: v.id("businessProGrants") },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const grant = await ctx.db.get(args.grantId);
if (!grant) {
throw new ConvexError({ kind: "GRANT_NOT_FOUND" });
}
if (grant.ownerUserId !== userId) {
throw new ConvexError({ kind: "NOT_OWNER" });
}
if (grant.status !== "pending" && grant.status !== "accepted") {
return { ok: true as const, status: "already_inactive" as const };
}
const now = Date.now();
// Serialize with inviteSeats via the per-Business-subscription lock row.
await touchBusinessSeatLock(ctx, grant.businessSubscriptionId, now);
await ctx.db.patch(args.grantId, { status: "revoked" });
if (grant.inviteeUserId) {
await ctx.runMutation(
internal.payments.subscriptionHelpers.recomputeEntitlementForUser,
{ userId: grant.inviteeUserId, eventTimestamp: now },View on GitHub (pinned to ffec79ac33)
Solutions
- Verify the grantId is a valid `businessProGrants` document ID that still exists before calling removeSeat
- Refresh the seat list in the UI before presenting the remove action
- Treat GRANT_NOT_FOUND as idempotent success (the seat is already gone) and reconcile the UI
Defensive patterns
Strategy: try-catch
Try / catch
try {
await convex.mutation(api.payments.businessSeats.removeSeat, { grantId });
} catch (err) {
if (err.data?.kind === 'GRANT_NOT_FOUND') {
// idempotent: seat already gone — sync UI and treat as success
} else if (err.data?.kind === 'NOT_OWNER') {
// not authorized to remove
} else { throw err; }
} Prevention
- Fetch the seat list before rendering a remove control so the grantId is current
- Treat GRANT_NOT_FOUND as idempotent success in the UI
- Guard removeSeat calls behind ownership checks in the UI
When it happens
Trigger: Calling `removeSeat({ grantId })` with an ID that is absent from `businessProGrants`, malformed, or belongs to a different table. Commonly hit when the UI shows a grant that another flow already revoked/deleted.
Common situations: Stale UI listing a grant removed elsewhere; copy/paste error in the grantId; race where a scheduled lapse job revoked the grant between page load and the remove click.
Related errors
- SEAT_CAP_REACHED
- NOT_OWNER
- Dodo checkout session ${session.session_id} has no checkout_
- OWNER_EMAIL_UNAVAILABLE
- OWNER_NOT_BUSINESS
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/12a355c7516c6a8d.
Report an issue: GitHub.