koala73/worldmonitor · error · ConvexError

NO_CUSTOMER

NO_CUSTOMER

Error message

NO_CUSTOMER

What it means

Thrown in `createCustomerPortalUrlForUser` when the user has no Dodo `customer_id` — either they have no subscription at all, or every subscription's `rawPayload` lacks a usable customer field. It is an object-typed ConvexError (`{ kind: "NO_CUSTOMER" }`) so the client surfaces the existing 'contact support' toast via `err.data.kind`.

Source

Thrown at convex/payments/billing.ts:442

 * the rawPayload and a same-user customers row still held the answer.
 */
export async function createCustomerPortalUrlForUser(
  ctx: Pick<ActionCtx, "runQuery">,
  userId: string,
): Promise<{ portal_url: string }> {
  const dodoCustomerId = await ctx.runQuery(
    internal.payments.billing.getDodoCustomerIdForUserPortal,
    { userId },
  );

  if (!dodoCustomerId) {
    // User has no subscription at all, or every sub's rawPayload lacks a
    // usable customer_id (very rare — would mean every webhook delivery
    // for this user dropped the customer field). Throw structured so
    // the client surfaces the existing "contact support" toast
    // (object-typed `data` so `err.data.kind` survives the wire — see
    // `api/_convex-error.js`).
    throw new ConvexError({ kind: "NO_CUSTOMER" });
  }

  const client = getDodoClient();
  let session;
  try {
    session = await client.customers.customerPortal.create(
      dodoCustomerId,
      { send_email: false },
    );
  } catch (err) {
    // The Dodo REST SDK throws a plain Error (APIError on a non-2xx Dodo
    // response, or a transport failure) when the portal-session create
    // fails. Convex's action runtime then masks any NON-ConvexError throw
    // as an opaque `[Request ID: X] Server Error`, dropping the real cause
    // from the wire — the exact opacity WORLDMONITOR-R5 fought for the
    // missing-customer path above (this was the last unwrapped throw site).
    // Re-throw as a structured ConvexError so the client receives
    // `err.data.kind === 'DODO_PORTAL_ERROR'` for proper Sentry

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Confirm the user actually has a Dodo subscription (check `subscriptions` table for their userId).
  2. If they should have one, inspect the stored `rawPayload` for a missing `customer_id` and re-process the originating webhook.
  3. Surface a 'contact support' / 'no active subscription' message client-side rather than retrying the portal call.
  4. For genuinely free users, hide the portal button until a subscription exists.

Example fix

// before
const { portal_url } = await getCustomerPortalUrl({});

// after
const subs = await listMySubscriptions({});
if (subs.length === 0) { showToast("No active subscription to manage."); return; }
const { portal_url } = await getCustomerPortalUrl({});
Defensive patterns

Strategy: validation

Validate before calling

const subs = await listMySubscriptions({});
if (subs.length === 0) {
  showToast("No active subscription to manage.");
  return;
}
await getCustomerPortalUrl({});

Try / catch

try {
  const { portal_url } = await getCustomerPortalUrl({});
} catch (e: any) {
  if (e?.data?.kind === "NO_CUSTOMER") {
    showToast("No subscription found. Contact support if this is unexpected.");
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: A user opens the billing/portal flow (`getCustomerPortalUrl` or the edge-gateway `internalGetCustomerPortalUrl`) but has never had a subscription created through Dodo, so no customer record resolves. Also fires in the rare case where all webhook deliveries for the user dropped the `customer_id` field.

Common situations: Free-tier or trial user clicking 'manage subscription' before ever paying; a user whose subscription was created off-platform; corrupted/incomplete webhook payload storage that lost `customer_id`.

Related errors


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