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

  1. In the edge gateway, validate the JWT `sub` is a non-empty string before calling the internal action.
  2. If `sub` is missing, reject the request with 401 at the edge instead of forwarding an empty userId.
  3. 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

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


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/adcc8077bd33315b. Report an issue: GitHub.