Skip to content

feat(billing): unify upgrade routing with reason context + storage/tables limit emails#5171

Open
waleedlatif1 wants to merge 8 commits into
stagingfrom
feat/usage-limit-upgrade-system
Open

feat(billing): unify upgrade routing with reason context + storage/tables limit emails#5171
waleedlatif1 wants to merge 8 commits into
stagingfrom
feat/usage-limit-upgrade-system

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Add a single upgrade-reason registry (lib/billing/upgrade-reasons.ts) — the source of truth for the language shown when a usage limit routes a user to the upgrade page. The same copy drives both the upgrade-page header and the threshold emails so they never drift.
  • Upgrade page reads ?reason= (nuqs) and swaps its header ("Upgrade to scale your tables", "…with your teammates", etc.); generic header when absent.
  • Every in-app limit surface now deep-links through buildUpgradeHref(workspaceId, reason): credits chip (credits), teammates (seats), tables row-limit toast (tables), file-upload storage error (storage, via a shared useLimitUpgradeToast). Generic "Explore plans" links (billing settings, deploy gate) route through the same helper without a reason.
  • New storage + tables threshold emails (80% warning / 100% reached) via one parameterized LimitThresholdEmail. Dedup is a race-free atomic claim (single conditional UPDATE … WHERE current < desired RETURNING against a new limit_notifications jsonb column on user_stats/organization, migration 0248), with hysteresis re-arm below 70%. One shared maybeNotifyLimit resolves user vs. org scope for both call sites.
  • Fix a latent bug: the existing credits threshold emails linked to a dead URL (/workspace?billing=upgrade, which redirects to home and drops the param). Re-pointed to the live upgrade/billing-settings routes.

Type of Change

  • New feature (+ bug fix for the dead credits link)

Notes

  • Seats threshold email is intentionally deferred — Team seats auto-scale (Stripe quantity, no cap) and enterprise seats are fixed but redirected away from the upgrade page, so there's no clean trigger. The seats in-app CTA + header ship now; the email infra (LimitCategory includes seats) is ready if the seat model changes.
  • Limit emails respect the same opt-outs as credits (getEmailPreferences + billingUsageNotificationsEnabled) and are best-effort/fire-and-forget so they never block a mutation.

Testing

  • Unit tests added: upgrade-reasons.test.ts (4) and limit-notifications.test.ts (9 — claim win/lose, dead band, re-arm, opt-outs, billing-disabled). Existing logger/tables suites green (60 tests total).
  • bun run check:api-validation, bun run check:react-query, typecheck, and biome all pass.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 23, 2026 4:32pm

Request Review

@cursor

cursor Bot commented Jun 22, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches billing notifications, DB state for dedup, and many upgrade entry points; behavior is mostly best-effort/async but incorrect links or duplicate emails would affect revenue UX.

Overview
Introduces a shared upgrade-reason registry (upgrade-reasons.ts) so limit CTAs, the upgrade page header, and email copy stay aligned. Upgrade links now go through buildUpgradeHref(workspaceId, reason?) with an optional ?reason= (credits, storage, tables, seats); the upgrade page reads that param (nuqs) and swaps the headline, with Suspense on the route.

In-app limit surfaces deep-link with context: credits chip, teammates invite gate, table row errors, and storage upload failures (new useLimitUpgradeToast with an Upgrade action). Credits threshold emails no longer use dead /workspace?billing=upgrade URLs—they point at workspace-scoped upgrade and billing settings when workspaceId is available.

Adds 80% / 100% threshold emails for storage and table rows via LimitThresholdEmail and maybeNotifyLimit, with dedup stored in new limit_notifications jsonb on user_stats / organization (migration 0248), atomic claim + re-arm below 70%. Hooks fire after storage tracking (with workspaceId) and table inserts/upserts.

Reviewed by Cursor Bugbot for commit f4a9626. Configure here.

Comment thread apps/sim/lib/billing/storage/tracking.ts
@greptile-apps

greptile-apps Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a unified upgrade-routing system with a central upgrade-reasons registry, and adds storage and table row usage-limit emails (80% warning / 100% reached). The atomic dedup mechanism using conditional UPDATE … WHERE current < desired RETURNING is race-free, and the hysteresis re-arm approach correctly prevents re-notification until usage drops below 70%.

  • Upgrade-reason registry (lib/billing/upgrade-reasons.ts): single source of truth for copy on both the upgrade page and limit threshold emails; buildUpgradeHref centralizes all deep-link construction, replacing scattered inline string templates.
  • Storage + tables threshold emails (lib/billing/core/limit-notifications.ts): per-category dedup via a new limit_notifications JSONB column on both user_stats and organization; fire-and-forget so emails never block mutations; per-recipient failures are isolated to avoid one bad address suppressing the rest.
  • Dead-link fix (lib/billing/core/usage.ts): existing credits threshold emails that previously linked to /workspace?billing=upgrade (a dropped param) now use buildUpgradeHref to route to the live upgrade or billing-settings page.

Confidence Score: 5/5

Safe to merge — the dedup claim is race-free, the re-arm hysteresis is correct, all email paths are fire-and-forget, and the migration is a backward-compatible additive column with a sensible default.

The atomic UPDATE … WHERE current < desired RETURNING claim correctly prevents duplicate emails even under concurrent calls. The re-arm path now receives priorUsage for the tables case and uses rearmOnly=true on the storage decrement path. Per-recipient email failures are isolated in individual try/catch blocks. The new limit_notifications JSONB column ships with NOT NULL DEFAULT '{}', so existing rows are handled without a backfill. Dead-link fix for credits emails is verified correct. No regressions found in any call site.

No files require special attention — the core notification logic, migration, email template, and routing changes all look correct.

Important Files Changed

Filename Overview
apps/sim/lib/billing/core/limit-notifications.ts Core notification logic — atomic claim, re-arm, and per-recipient isolation all look correct; matches the pattern from the credits path in usage.ts
apps/sim/lib/billing/upgrade-reasons.ts Clean registry pattern; buildUpgradeHref and isUpgradeReason are well-typed; credits email subject fields in the registry are never used by getLimitEmailSubject (credits uses the old path) but this is documented intent
apps/sim/lib/billing/storage/tracking.ts Correct fire-and-forget storage notification wired after the DB update; priorUsage not passed (by design — post-increment re-reads correct org/user totals via getUserStorageUsage)
apps/sim/lib/table/billing.ts Table row notification correctly passes pre-insert count as priorUsage to enable re-arm on large jumps, addressing the previously flagged structural issue
packages/db/migrations/0248_limit_notifications.sql Additive migration — two NOT NULL DEFAULT '{}'::jsonb columns on organization and user_stats; safe to apply without downtime
apps/sim/lib/billing/core/usage.ts Dead-link fix for credits threshold emails: /workspace?billing=upgrade replaced with live upgrade and billing-settings URLs via buildUpgradeHref
apps/sim/components/emails/billing/limit-threshold-email.tsx Parameterized email template driven by UPGRADE_REASON_COPY; consistent with existing email layout and style patterns
apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx Upgrade page now reads ?reason= via nuqs and swaps the h1 header from UPGRADE_REASON_COPY; Suspense boundary correctly added in page.tsx for the useQueryState call
apps/sim/lib/billing/core/limit-notifications.test.ts 14 tests covering claim win/lose, dead band, re-arm, opt-outs, billing-disabled, zero usage, and wipe-then-rebuild; mock setup correctly isolates DB and email side-effects
apps/sim/lib/billing/client/use-limit-upgrade-toast.ts Simple hook that wraps buildUpgradeHref into an actionable toast; correctly uses useParams within the [workspaceId] route context

Reviews (5): Last reviewed commit: "fix(billing): resolve recipients before ..." | Re-trigger Greptile

Comment thread apps/sim/lib/table/billing.ts Outdated
Comment thread apps/sim/lib/billing/core/limit-notifications.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/core/limit-notifications.ts Outdated
Comment thread apps/sim/lib/billing/core/limit-notifications.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/storage/tracking.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/billing/core/limit-notifications.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/table/billing.ts Outdated
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b734f34. Configure here.

currentRowCount: params.currentRowCount,
addedRows: params.addedRows,
limit,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Table emails before commit

Medium Severity

notifyTableRowUsage runs inside assertRowCapacity as soon as the capacity check passes, before callers finish their insert transaction. If a later validation or DB step rolls back, threshold emails can still send and the atomic claim can advance limit_notifications even though no rows were added.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b734f34. Configure here.

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant