Uh oh!
There was an error while loading. Please reload this page.
feat(billing): unify upgrade routing with reason context + storage/tables limit emails - #5171
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Adds 80% / 100% threshold emails for storage and table row limits via Minor: Pi block icon/branding tweak; upgrade page wrapped in Reviewed by Cursor Bugbot for commit 79a4d08. Configure here. |
Uh oh!
There was an error while loading. Please reload this page.
Greptile SummaryThis PR unifies upgrade routing with a central reason registry (
Confidence Score: 5/5Safe to merge — the notification path is fully fire-and-forget and never blocks mutations; the migration adds non-destructive columns with safe defaults; previously flagged concurrency and re-arm bugs are addressed. The new dedup logic (single atomic conditional UPDATE, mutually exclusive re-arm/claim per call, isolated per-recipient failures) is sound. The edge-triggered apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts — the deleteWorkspaceFile path does not pass workspaceId to decrementStorageUsage, so storage threshold re-arm is skipped after file deletion. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as Upload / Insert
participant T as tracking.ts / billing.ts
participant NL as maybeNotifyLimit
participant ML as maybeSendLimitThresholdEmail
participant DB as DB (user_stats / organization)
participant E as Email
U->>T: "incrementStorageUsage(userId, bytes, workspaceId)<br/>OR notifyTableRowUsage(workspaceId, current, added, limit)"
T->>T: edge-trigger check (crossedUp into 80% / 100% band?)
alt no threshold crossing
T-->>U: return (no-op)
else threshold crossed
T->>NL: maybeNotifyLimit(category, billedUserId, usage, limit)
NL->>NL: resolve org vs user scope
NL->>ML: maybeSendLimitThresholdEmail(scope, usage, limit, ...)
alt "percent < 70% (re-arm band)"
ML->>DB: "rearmThreshold: UPDATE SET stored=0 WHERE stored>0"
ML-->>NL: return (no email)
else "70% <= percent < 80% (dead band)"
ML-->>NL: return (no-op)
else "percent >= 80%"
ML->>ML: resolveRecipients (opt-out checks)
alt no eligible recipients
ML-->>NL: return (claim not burned)
else recipients found
ML->>DB: "claimThreshold: UPDATE SET stored=desired WHERE stored < desired RETURNING"
alt claim lost
DB-->>ML: [] (0 rows)
ML-->>NL: return
else claim won
DB-->>ML: "[{id}]"
loop per recipient (failures isolated)
ML->>E: renderLimitThresholdEmail + sendEmail
end
end
end
end
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as Upload / Insert
participant T as tracking.ts / billing.ts
participant NL as maybeNotifyLimit
participant ML as maybeSendLimitThresholdEmail
participant DB as DB (user_stats / organization)
participant E as Email
U->>T: "incrementStorageUsage(userId, bytes, workspaceId)<br/>OR notifyTableRowUsage(workspaceId, current, added, limit)"
T->>T: edge-trigger check (crossedUp into 80% / 100% band?)
alt no threshold crossing
T-->>U: return (no-op)
else threshold crossed
T->>NL: maybeNotifyLimit(category, billedUserId, usage, limit)
NL->>NL: resolve org vs user scope
NL->>ML: maybeSendLimitThresholdEmail(scope, usage, limit, ...)
alt "percent < 70% (re-arm band)"
ML->>DB: "rearmThreshold: UPDATE SET stored=0 WHERE stored>0"
ML-->>NL: return (no email)
else "70% <= percent < 80% (dead band)"
ML-->>NL: return (no-op)
else "percent >= 80%"
ML->>ML: resolveRecipients (opt-out checks)
alt no eligible recipients
ML-->>NL: return (claim not burned)
else recipients found
ML->>DB: "claimThreshold: UPDATE SET stored=desired WHERE stored < desired RETURNING"
alt claim lost
DB-->>ML: [] (0 rows)
ML-->>NL: return
else claim won
DB-->>ML: "[{id}]"
loop per recipient (failures isolated)
ML->>E: renderLimitThresholdEmail + sendEmail
end
end
end
end
end
Reviews (14): Last reviewed commit: "docs(billing): drop self-explanatory inl..." | Re-trigger Greptile |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Jun 22, 2026
waleedlatif1
commented
Jun 22, 2026
@cursor review |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Jun 22, 2026
waleedlatif1
commented
Jun 22, 2026
@cursor review |
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Jun 22, 2026
waleedlatif1
commented
Jun 22, 2026
@cursor review |
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Jun 22, 2026
waleedlatif1
commented
Jun 22, 2026
@cursor review |
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Jun 23, 2026
waleedlatif1
commented
Jun 23, 2026
@cursor review |
Uh oh!
There was an error while loading. Please reload this page.
waleedlatif1
commented
Jun 23, 2026
waleedlatif1
commented
Jun 23, 2026
@cursor review |
… never emails or burns the claim
…aim (no duplicate-email race)
edfe73e to
d43e683Compare…bles warn-once behavior
waleedlatif1
commented
Jun 23, 2026
waleedlatif1
commented
Jun 23, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a7ae72d. Configure here.
waleedlatif1
commented
Jun 23, 2026
waleedlatif1
commented
Jun 23, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f467991. Configure here.
waleedlatif1
commented
Jun 23, 2026
waleedlatif1
commented
Jun 23, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 375317b. Configure here.
waleedlatif1
commented
Jun 23, 2026
waleedlatif1
commented
Jun 23, 2026
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 79a4d08. Configure here.
waleedlatif1
commented
Jun 23, 2026
@greptile re-review latest commit |
Uh oh!
There was an error while loading. Please reload this page.
Summary
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.?reason=(nuqs) and swaps its header ("Upgrade to scale your tables", "…with your teammates", etc.); generic header when absent.buildUpgradeHref(workspaceId, reason): credits chip (credits), teammates (seats), tables row-limit toast (tables), file-upload storage error (storage, via a shareduseLimitUpgradeToast). Generic "Explore plans" links (billing settings, deploy gate) route through the same helper without a reason.LimitThresholdEmail. Dedup is a race-free atomic claim (single conditionalUPDATE … WHERE current < desired RETURNINGagainst a newlimit_notificationsjsonb column onuser_stats/organization, migration0248), with hysteresis re-arm below 70%. One sharedmaybeNotifyLimitresolves user vs. org scope for both call sites./workspace?billing=upgrade, which redirects to home and drops the param). Re-pointed to the live upgrade/billing-settings routes.Type of Change
Notes
LimitCategoryincludesseats) is ready if the seat model changes.getEmailPreferences+billingUsageNotificationsEnabled) and are best-effort/fire-and-forget so they never block a mutation.Testing
upgrade-reasons.test.ts(4) andlimit-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