langfuse/langfuse · error · TRPCError

INTERNAL_SERVER_ERROR

INTERNAL_SERVER_ERROR

Error message

Organization not found

What it means

resolveBillingService loads the organization from Postgres to decide which billing backend (Stripe vs ClickHouse Billing) serves it; if findUnique returns null it throws INTERNAL_SERVER_ERROR 'Organization not found'. The orgId came from a caller that assumed the org exists, so this signals stale or forged input rather than a billing problem.

Source

Thrown at web/src/ee/features/billing/server/resolveBillingService.ts:49

 * never a half-configured CHB flow. Orgs already carrying CHB state cannot be
 * served by Stripe and error instead.
 *
 * This dispatch is also the structural interlock that keeps Stripe checkout
 * unreachable for any org holding a `cloudConfig.clickhouse` block: such
 * orgs always resolve to the CHB service.
 */
export const resolveBillingService = async (
  ctx: OrgAuthedContext,
  orgId: string,
): Promise<{
  billingProvider: BillingProvider;
  service: CloudBillingService;
}> => {
  const org = await ctx.prisma.organization.findUnique({
    where: { id: orgId },
  });
  if (!org) {
    throw new TRPCError({
      code: "INTERNAL_SERVER_ERROR",
      message: "Organization not found",
    });
  }
  const parsedOrg = parseDbOrg(org);
  // The cutoff is injected rather than read inside the resolver: the shared
  // module is exported from the client-safe barrel, so web passes the value from
  // its own validated env schema.
  const cutoff = env.LANGFUSE_CLOUD_BILLING_CHB_CUTOFF_DATE;
  const billingProvider = getBillingProvider(parsedOrg, { cutoff });

  if (billingProvider === "clickhouse") {
    // Shared process-wide, so every billing request reuses one Auth0 token
    // cache rather than minting a token per request.
    const client = getChbApiClient();
    if (!client) {
      if (parsedOrg.cloudConfig?.clickhouse?.organizationId) {
        // Sticky CHB org: Stripe cannot serve it, this is a config error

View on GitHub (pinned to 59d92c7cf3)

Solutions

  1. Verify the orgId is a real organization id from the same tenant/session before calling billing APIs.
  2. Handle org-deletion races upstream by invalidating sessions/caching when an org is removed.
  3. Consider NOT_FOUND semantics: this is arguably a 404 and callers should treat missing-org as expected, not a 500.

Example fix

// before
const svc = await resolveBillingService(ctx, maybeStaleOrgId);
// after
const org = await ctx.prisma.organization.findUnique({ where: { id: orgId } });
if (!org) throw new TRPCError({ code: "NOT_FOUND", message: "Organization not found" });
const svc = await resolveBillingService(ctx, orgId);
Defensive patterns

Strategy: validation

Validate before calling

const org = await ctx.prisma.organization.findUnique({
  where: { id: orgId, members: { some: { userId: session.user.id } } },
});
if (!org) throw new TRPCError({ code: "NOT_FOUND" });

Try / catch

catch (e) { if (e instanceof TRPCError && e.code === "INTERNAL_SERVER_ERROR" && e.message === "Organization not found") { /* treat as stale org, refresh session */ } throw e; }

Prevention

When it happens

Trigger: Passing an orgId to resolveBillingService (transitively any billing service method) for an organization that was deleted, or a typo'd/mismatched org id (e.g. project id passed as orgId).

Common situations: Org deleted while a user had a billing page open; race between org deletion and in-flight requests; tests using fixture ids that were never created.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of langfuse/langfuse@59d92c7cf3 (2026-08-27). Data as JSON: /api/errors/4501f76c5c057c38. Report an issue: GitHub.