Prompt file imported from Idoiv/hearflow (
.github/prompts/review-pricing-tiers.prompt.md). Copyright stays with the author.
Review: Pricing & Tier Limits Implementation
You are a senior engineer reviewing Hearflow's pricing, billing, and tier-limit enforcement system. Perform a thorough audit of correctness, consistency, security, and edge-case handling.
Scope
The billing system spans these layers — read every file before drawing conclusions:
Tier Definitions & Entitlements
apps/web/core/billing/plans.ts— plan display metadata (prices, names, features)apps/web/core/billing/entitlements.ts— per-tier numeric limits and boolean feature flagspackages/shared/constants/team.ts—TEAM_MEMBER_LIMITSby tier
Limit Enforcement
apps/web/core/billing/checkEntitlement.ts— web-app entitlement guard (Redis metered + Supabase static)apps/web/workers/utils/insightQuota.ts— worker-side insight quota (IORedis)apps/web/core/billing/clampResources.ts— soft-disable on downgrade (integrations, members)supabase/migrations/20260322000001_team_seat_limits.sql— DB-level seat limit enforcement
Stripe Integration
apps/web/core/billing/stripe.ts— Stripe client, price-ID maps, tier↔price resolutionapps/web/core/billing/stripePriceCatalog.ts— dynamic price fetching with fallbackapps/web/app/api/v1/billing/checkout/route.ts— checkout session creationapps/web/app/api/v1/billing/portal/route.ts— customer portal sessionapps/web/app/api/v1/billing/webhook/route.ts— webhook ingestion + idempotencyapps/web/app/api/v1/billing/reconcile/— post-checkout reconciliationapps/web/app/api/v1/billing/trial-swap/route.ts— trial tier swap (no Stripe)
Webhook Processing
apps/web/workers/process-stripe-webhook.ts— Trigger.dev task for webhook processingapps/web/workers/handlers/billing/stripeEventHandlers.ts— per-event-type handler logic
Scheduled Tasks
apps/web/workers/schedules/expireTrials.ts— daily trial expiration cronapps/web/workers/schedules/applyDeferredDowngrades.ts— hourly deferred downgrade applicationapps/web/workers/schedules/reconcileFailedStripeEvents.ts— retry failed webhook events
Frontend
apps/web/core/billing/SubscriptionContext.tsx— React context for tier/status- Plan cards, upgrade prompts, usage meters (search
components/for billing UI)
AI Budget (Tier-Adjacent)
apps/web/workers/infrastructure/aiBudget.ts— 500K tokens/day, 100 calls/day per companyapps/web/workers/pipeline/config.ts—AI_BUDGET_LIMITS
Database
companiestable columns:subscription_tier,subscription_status,trial_ends_at,billing_period,stripe_customer_id,stripe_subscription_id,pending_subscription_tier,pending_downgrade_atstripe_eventstable for webhook idempotency
Review Checklist
Work through each category. For every finding, cite the exact file and line. Classify severity as CRITICAL / HIGH / MEDIUM / LOW.
1. Tier Limit Consistency
- Limits in
entitlements.tsmatchplans.tsfeature display (no UI claiming features the backend doesn't enforce) -
TEAM_MEMBER_LIMITSinpackages/shared/constants/team.tsmatchesentitlements.ts - DB-level seat check (
get_team_seat_limit) matches TypeScript constants - Free tier genuinely blocks paid features (CSV export, audit log, API access, MCP, SSO)
- Enterprise tier has no artificial caps (unlimited where documented)
- Backfill days per tier are consistent across entitlements, integration polling config, and cron tasks
2. Enforcement Completeness
- Every metered resource (insights/month) has both increment and check logic
- Redis key format is consistent (
{resource}:{companyId}:{period}) - TTL on Redis counters is correct (monthly counters expire after billing period, daily AI budget after 48h)
- Static limits (integrations, members) are checked before creation, not just in UI
-
checkEntitlementis called in every relevant API route (not just some) - Worker-side quota (
insightQuota.ts) and web-side quota (checkEntitlement.ts) use the same limits - Fail-open vs fail-closed strategy is intentional and documented for each resource type
3. Stripe Correctness
-
priceIdToTier()handles all 4 price IDs (starter monthly, starter annual, pro monthly, pro annual) - Checkout rejects: same-tier purchase, downgrade purchase, enterprise self-serve
- Checkout metadata includes
companyId,tier,billingPeriodfor webhook processing - Annual price IDs are actually recurring (not one-time) — see past incident in repo memory
-
stripeIntervalToBillingPeriod()handlesmonthandyearcorrectly - Dynamic price catalog fallback returns correct values when Stripe API is down
- Webhook signature verification uses raw body (not parsed JSON)
4. Webhook & Event Processing
- All relevant Stripe events are handled:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted,invoice.payment_failed,invoice.paid,customer.subscription.trial_will_end - Idempotency: duplicate events with same
event_idare safely rejected - CAS guards prevent concurrent webhook race conditions on
companiesrow -
handleSubscriptionUpdatedcorrectly implements deferred downgrade (D2) — storespending_subscription_tier+pending_downgrade_atinstead of immediate tier change - Enterprise guard (D4): webhooks never downgrade an enterprise company
-
handleSubscriptionDeleteddowngrades to free and clamps resources -
handleInvoicePaymentFailedsetspast_duestatus without removing access - Failed events are retried by reconciliation cron (max 3 attempts, 15-min cooldown)
- Stale
processingevents are reclaimed after timeout (5 minutes)
5. Trial Logic
- Signup correctly assigns trial status +
trial_ends_atfor paid tier signups - Free tier signup gets
activestatus (no trial) -
expireTrialscron: properly filtersstatus = 'trial' AND trial_ends_at < NOW() - Expired trial → free tier transition clamps integrations and demotes excess members
- Trial → paid conversion (checkout) clears
trial_ends_atand setsactive -
trial-swapAPI validates company is in trial status with no Stripe subscription - Trial companies cannot access paid features beyond their trial tier's entitlements
6. Downgrade & Clamping
-
clampResourcesdisables excess integrations (oldest first) — no data deleted -
clampResourcesdemotes excess members to viewer (most recent first) — owners never demoted - Deferred downgrade (
applyDeferredDowngradescron) applies at billing period end, not immediately - Immediate downgrade only on subscription deletion, not on plan change
-
pending_subscription_tieris cleared on successful checkout (prevents stale pending state) - Batch processing in
applyDeferredDowngradeshandles pagination correctly (50/batch, max 20 iterations)
7. Security
- All billing API routes use
withAdminAuth(admin-only) - Webhook route validates Stripe signature before processing
- No
getSession()calls in billing routes (must usegetUser()) - Checkout
success_urlandcancel_urluse absolute URLs fromNEXT_PUBLIC_APP_URL - Stripe customer/subscription IDs are not exposed to non-admin users
-
stripe_eventstable has RLS enabled - Service role usage (if any) is documented in
docs/security/SERVICE_ROLE_EXCEPTIONS.md - Reconcile endpoint has CSRF protection
- No price manipulation possible via client-side parameters (prices from server-side config only)
8. Edge Cases & Race Conditions
- What happens if a trial company starts checkout but the webhook arrives after trial expiry cron?
- What happens if two webhooks for the same subscription arrive simultaneously?
- What if
past_due→invoice.paidarrives while a deferred downgrade is pending? - What if Redis is down during insight extraction — does the AI budget correctly block?
- What if a company has exactly the tier limit of integrations and a downgrade happens?
- What if
billing_periodis NULL (legacy free-tier companies)? - What happens when switching from annual to monthly billing mid-cycle?
9. Observability
- Billing state changes are logged with structured context (company ID, old tier, new tier, event ID)
- Failed webhook processing surfaces in Sentry or monitoring
- Quota near-limit warnings exist (or should they?)
- Trial expiration emails are sent before and after expiry
- Deferred downgrade application is logged
Output Format
## Findings
### [SEVERITY] Title
**File:** path/to/file.ts#L42
**Issue:** Description of the problem
**Impact:** What could go wrong
**Fix:** Suggested remediation
---
## Optional Follow-Ups / Nits
### [LOW] Title
...
---
## Summary
- CRITICAL: N
- HIGH: N
- MEDIUM: N
- LOW: N
- Edge cases needing attention: [list]
Important Notes
- Read
docs/development/CODE_REVIEW_FALSE_POSITIVES.mdbefore flagging issues — some patterns are intentional. - Check the actual implementation before claiming a bug exists. Cite file + line.
- Verify imports exist before flagging missing functionality.
- Prefer codebase patterns over general best-practice assumptions.
- Do NOT flag: defensive coding that exists by design, fail-open strategies that are documented, or Stripe-specific patterns that follow their official docs.