{"record":{"id":"21d9425f9274b971","repo":"kriasoft/react-starter-kit","slug":"unknown-plan-plan","errorCode":null,"errorMessage":"Unknown plan \"${plan}\"","messagePattern":"Unknown plan \"(.+?)\"","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/api/routers/billing.ts","lineNumber":76,"sourceCode":"      }\n\n      canManage = canManageOrgBilling(membership.role);\n    }\n\n    const referenceId = organizationId ?? ctx.user.id;\n\n    const sub = await ctx.db.query.subscription.findFirst({\n      where: (s, { eq, and, inArray }) =>\n        and(\n          eq(s.referenceId, referenceId),\n          inArray(s.status, [\"active\", \"trialing\"]),\n        ),\n    });\n\n    const plan = sub?.plan ?? \"free\";\n\n    if (!(plan in planLimits)) {\n      throw new Error(`Unknown plan \"${plan}\"`);\n    }\n\n    return {\n      enabled,\n      canManage,\n      plan,\n      status: sub?.status ?? null,\n      periodEnd: sub?.periodEnd ?? null,\n      cancelAtPeriodEnd: sub?.cancelAtPeriodEnd ?? false,\n      limits: planLimits[plan as PlanName],\n    };\n  }),\n});\n","sourceCodeStart":58,"sourceCodeEnd":90,"githubUrl":"https://github.com/kriasoft/react-starter-kit/blob/0aa7603435f16159ad0b8fef68fb7f6280be7ca1/apps/api/routers/billing.ts#L58-L90","documentation":"After resolving the subscription, plan = sub?.plan ?? 'free' is checked against the planLimits lookup table (lib/plans.ts). If the stored subscription plan string is not a known key, this plain Error throws, guarding planLimits[plan as PlanName] from reading an undefined limits object. It is a data-integrity check, not an input-validation error.","triggerScenarios":"A subscription row in the database has a plan value not present in planLimits — e.g. Stripe webhook wrote a price/product name that doesn't map to a known plan key, or plans.ts was edited/renamed without migrating existing subscription rows.","commonSituations":"Renaming a plan in lib/plans.ts while old subscriptions keep the old string, adding a new Stripe price in Terraform without adding it to planLimits, or a webhook handler storing planId instead of the mapped plan name.","solutions":["Inspect the offending subscription row (SELECT reference_id, plan, status FROM subscription WHERE ...) and see what value was stored","Add the missing plan key to planLimits in lib/plans.ts (and its limits), or fix the row with a migration/UPDATE","Align the Stripe webhook mapping so price IDs map to exactly the plan keys defined in plans.ts","Narrow the type: validate sub.plan against PlanName at write time (webhook) instead of only at read time"],"exampleFix":"// before\n// lib/plans.ts had { free, starter, pro } but DB row has plan: 'starter_v2'\n// after\nexport const planLimits = { free: ..., starter: ..., pro: ..., starter_v2: ... } satisfies Record<PlanName, Limits>;\n// or fix the row:\n// UPDATE subscription SET plan = 'starter' WHERE plan = 'starter_v2';","handlingStrategy":"validation","validationCode":"import { planLimits } from '../lib/plans.js';\nfunction assertKnownPlan(plan: string): asserts plan is keyof typeof planLimits {\n  if (!(plan in planLimits)) {\n    throw new Error(`Unknown plan \"${plan}\" — add it to planLimits or migrate the subscription row`);\n  }\n}\n// run against the DB before reading billing:\n// SELECT DISTINCT plan FROM subscription WHERE status IN ('active','trialing');","typeGuard":"function isKnownPlan(plan: string): plan is keyof typeof planLimits {\n  return plan in planLimits;\n}","tryCatchPattern":"try {\n  const billing = await trpc.billing.subscription.query();\n  return billing;\n} catch (error) {\n  if (error instanceof Error && /^Unknown plan /.test(error.message)) {\n    console.error('Plan data mismatch between DB and plans.ts:', error.message);\n    return { enabled: true, canManage: false, plan: 'free', limits: fallbackLimits }; // degrade gracefully\n  }\n  throw error;\n}","preventionTips":["Keep planLimits in sync with every Stripe price ID added in Terraform/config","When renaming a plan, write a DB migration updating existing subscription rows in the same deploy","In the webhook handler, validate incoming plan values against planLimits before persisting","Add a CI/test that asserts every plan string in seed/fixture data exists in planLimits"],"tags":["billing","data-integrity","stripe","plans"],"backgroundTag":"unknown-plan-value","analyzedSha":"0aa7603435f16159ad0b8fef68fb7f6280be7ca1","analyzedAt":"2026-08-31T21:50:55.742Z","schemaVersion":2},"datasetVersion":"2026-08-31T22:30:34.772Z"}