koala73/worldmonitor · error · ConvexError
USER_ID_REQUIRED
USER_ID_REQUIRED
Error message
USER_ID_REQUIRED
What it means
Thrown by the `internalGetCustomerPortalUrl` internal action (called from the edge gateway after Clerk JWT verification) when `args.userId` is empty/falsy. It is an object-typed ConvexError (`{ kind: "USER_ID_REQUIRED" }`). This guards the portal-creation path that runs without Convex's own auth context, so it must be passed an explicit userId.
Source
Thrown at convex/payments/billing.ts:3678
* Public action callable from the browser. Auth-gated via requireUserId(ctx).
*/
export const getCustomerPortalUrl = action({
args: {},
handler: async (ctx, _args) => {
const userId = await requireUserId(ctx);
return createCustomerPortalUrlForUser(ctx, userId);
},
});
/**
* Internal action callable from the edge gateway to create a user-scoped
* Dodo Customer Portal session after the Clerk JWT has been verified there.
*/
export const internalGetCustomerPortalUrl = internalAction({
args: { userId: v.string() },
handler: async (ctx, args) => {
if (!args.userId) {
throw new ConvexError({ kind: "USER_ID_REQUIRED" });
}
return createCustomerPortalUrlForUser(ctx, args.userId);
},
});
// ---------------------------------------------------------------------------
// Subscription claim (anon ID → authenticated user migration)
// ---------------------------------------------------------------------------
/**
* Claims subscription, entitlement, and customer records from an anonymous
* browser ID to the currently authenticated user.
*
* LIMITATION: Until Clerk auth is wired into the ConvexClient, anonymous
* purchases are keyed to a `crypto.randomUUID()` stored in localStorage
* (`wm-anon-id`). If the user clears storage, switches browsers, or later
* creates a real account, there is no automatic way to link the purchase.
*View on GitHub (pinned to ffec79ac33)
Solutions
- In the edge gateway, validate the JWT `sub` is a non-empty string before calling the internal action.
- If `sub` is missing, reject the request with 401 at the edge instead of forwarding an empty userId.
- Confirm the Clerk JWT template includes the `sub` claim.
Example fix
// before (edge gateway)
await convex.query(internal.payments.billing.internalGetCustomerPortalUrl, { userId: jwt.sub ?? "" });
// after
const userId = jwt.sub;
if (!userId) return new Response("Unauthorized", { status: 401 });
await convex.query(internal.payments.billing.internalGetCustomerPortalUrl, { userId }); Defensive patterns
Strategy: validation
Validate before calling
// In the edge gateway
const userId = verifiedJwt.sub;
if (!userId || typeof userId !== "string") {
return new Response("Unauthorized", { status: 401 });
}
await convex.query(internal.payments.billing.internalGetCustomerPortalUrl, { userId }); Type guard
function isNonEmptyUserId(s: unknown): s is string {
return typeof s === "string" && s.length > 0;
} Prevention
- Validate JWT `sub` is a non-empty string at the edge before calling the internal action.
- Ensure the Clerk JWT template exposes the `sub` claim.
When it happens
Trigger: The edge gateway extracts the subject from a verified Clerk JWT but passes an empty string (JWT had no `sub` claim, or extraction returned undefined/empty) into `internalGetCustomerPortalUrl({ userId })`.
Common situations: Edge handler received a JWT missing the `sub` claim; a code path that reads `userId` from the wrong field or falls back to empty; the gateway was called from a test/stub without a real subject.
Related errors
- ANON_CLAIM_PROOF_REQUIRED
- NO_CUSTOMER
- OWNER_EMAIL_UNAVAILABLE
- OWNER_NOT_BUSINESS
- OWNER_DOMAIN_NOT_CORPORATE
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/adcc8077bd33315b.
Report an issue: GitHub.