You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
modules/billing/services/billing.webhook.service.jscustomer.subscription.updated handler calls resolvePlan(subscription) which reads price.metadata.planId. price.metadata.planId is empty in real Stripe webhook payloads — Stripe only populates metadata on the Product object, not on the Price object surfaced through the Subscription update event.
Result: every paid org's plan gets reset to `free` on every Stripe `customer.subscription.updated` event. Every devkit downstream running Stripe billing (pierreb, comes, montaine, ism) is silently affected in production today.
Trawl fixed this in comes-io/trawl_node#1250 by building a priceId → plan map from config.stripe.prices and using that map instead. This issue ports the fix upstream so every downstream benefits via /update-stack.
Fix
Port from trawl:
buildPriceIdToPlanMap() — builds Map<priceId, planName> from config.stripe.prices at boot
Rewritten resolvePlan(subscription) — looks up plan via priceId map first; falls back to price.metadata.planId for legacy test fixtures; final fallback free with warn log
Problem
modules/billing/services/billing.webhook.service.jscustomer.subscription.updatedhandler callsresolvePlan(subscription)which readsprice.metadata.planId.price.metadata.planIdis empty in real Stripe webhook payloads — Stripe only populates metadata on the Product object, not on the Price object surfaced through the Subscription update event.Result: every paid org's plan gets reset to `free` on every Stripe `customer.subscription.updated` event. Every devkit downstream running Stripe billing (pierreb, comes, montaine, ism) is silently affected in production today.
Trawl fixed this in
comes-io/trawl_node#1250by building apriceId → planmap fromconfig.stripe.pricesand using that map instead. This issue ports the fix upstream so every downstream benefits via/update-stack.Fix
Port from trawl:
buildPriceIdToPlanMap()— buildsMap<priceId, planName>fromconfig.stripe.pricesat bootresolvePlan(subscription)— looks up plan via priceId map first; falls back toprice.metadata.planIdfor legacy test fixtures; final fallbackfreewith warn logcancelAt/cancelAtPeriodEndsync oncustomer.subscription.updated(writes to fields added in feat(billing): subscription cancelAt + cancelAtPeriodEnd lifecycle fields #3741)handleSubscriptionCreatedsafety-net handler — race-safe Dashboard/Payment Link subscription creationretryWithBackoff+isNonTransientStripeError— new lib filesbilling.retry.js+billing.stripe-errors.js(port verbatim from trawl)Strip trawl-specific
analytics.capture('subscription_changed')calls.Labels
P0, billing, stripe
References
2026-05-30-trawl-devkit-perfect-alignment.mdTask C.2comes-io/trawl_node#1250