Skip to content

Stripe webhooks, deeper billing UX, and email delivery alerts - #940

Merged
kody-bot merged 8 commits into
mainfrom
cursor/stripe-webhooks-billing-alerts-0731
Jul 25, 2026
Merged

kody-bot merged 8 commits into
mainfrom
cursor/stripe-webhooks-billing-alerts-0731

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Three invite-cohort readiness follow-ups, orchestrated as parallel tracks then integrated:

  1. Stripe platform webhooks at POST /webhooks/stripe
    • Signature verification (STRIPE_WEBHOOK_SECRET), process-then-record idempotency (stripe_webhook_events), 30-day retention prune
    • Handles checkout.session.completed (link customer without relying on success redirect), customer.subscription.updated/deleted, invoice.payment_failed
    • Cron poll kept as backup; refresh now returns subscriptionStatus
  2. Deeper /account/billing UX — status badge (incl. past_due), clear Subscribe / Manage / View usage actions, webhook-aware copy
  3. Email delivery reputation alert — hourly admin email when complained/bounced burst (≥20/hour), KV cooldown, complements per-user outbound pause

Stripe ops (already done in this agent run)

  • Created live webhook endpoint we_1TxB2NLAQpAnsYszAr7S6krK → https://heykody.dev/webhooks/stripe with the four events above
  • Stored signing secret as GitHub Actions secret STRIPE_WEBHOOK_SECRET (deploy.yml syncs via --set-from-env-optional)

Test plan

  • Focused unit/workers tests for webhook signature, event processing, billing loader, email alerts, retention prune
  • typecheck, lint, format:check, migrations:check, primitives:check
  • CI npm run validate
  • After deploy: Stripe Dashboard → webhook deliveries succeed; checkout without hitting success URL still links plan; /account/billing shows status
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 367cd150 · Head: 1cc057c5

Classification: extends — Stripe billing gains signed webhook ingress + idempotency table; scheduled cron gains a second ops alert; account billing UI surfaces subscription status.

Primitives touched

Primitive Group Impact
billing auth extends — webhook receiver, signature verify, event→plan sync
d1-app-db storage extends — stripe_webhook_events + retention prune
scheduled-cron surfaces extends — email delivery reputation burst alert
app-ui surfaces extends — /account/billing status + CTA depth
entitlements auth composes — test schema only

System map

Stripe events enter a signed webhook, update per-user stripe_plan, and the billing UI reads refreshed status; a separate hourly cron pages admins on email reputation bursts.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
  stripe["Stripe"]:::untouched
  billing["billing<br/>Stripe billing"]:::extended
  d1["d1-app-db<br/>D1 app database"]:::extended
  appUi["app-ui<br/>Account UI"]:::extended
  cron["scheduled-cron<br/>Scheduled cron"]:::extended
  stripe -->|"POST /webhooks/stripe signed"| billing
  billing -->|"stripe_plan + webhook event ids"| d1
  appUi -->|"refresh + subscriptionStatus"| billing
  cron -->|"email delivery burst email"| d1
  classDef touched fill:#1a7f37,color:#fff
  classDef extended fill:#9a6700,color:#fff
  classDef added fill:#cf222e,color:#fff
  classDef untouched fill:#57606a,color:#fff
Loading

Change flow

sequenceDiagram
  participant Stripe
  participant Webhook as POST /webhooks/stripe
  participant Sync as subscription-sync
  participant D1
  Stripe->>Webhook: signed event
  Webhook->>Webhook: verify Stripe-Signature
  Webhook->>Sync: link/refresh by event type
  Sync->>D1: users.stripe_plan
  Webhook->>D1: insert stripe_webhook_events
Loading

Invariants

  • Per-user isolation unchanged: webhook attribution uses HMAC client_reference_id / linked stripe_customer_id; alerts use aggregate delivery counts + admin emails only.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added a signed Stripe webhook endpoint to handle billing-related events with signature verification, safe behavior when the secret is unset, and idempotent processing.
    • Updated the billing page to surface subscription status and gate Subscribe/Manage subscription actions, plus added a Usage shortcut.
    • Introduced an hourly admin alert for bounced/complained email spikes with cooldown throttling.
  • Documentation
    • Documented STRIPE_WEBHOOK_SECRET and updated billing/retention/setup guidance.
  • Tests
    • Expanded automated coverage for webhook signing/processing, billing data refresh, retention pruning, and email alert cron behavior.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 21 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 941e90e9-fa5c-451b-afe8-deea12dbe042

📥 Commits

Reviewing files that changed from the base of the PR and between e6476db and 9184c6d.

📒 Files selected for processing (37)
  • .github/workflows/deploy.yml
  • docs/contributing/architecture/data-storage.md
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/architecture/primitives.yaml
  • docs/contributing/environment-variables.md
  • docs/contributing/security.md
  • docs/contributing/setup-manifest.md
  • packages/worker/.env.example
  • packages/worker/client/routes/account-billing.tsx
  • packages/worker/migrations/0093-stripe-webhook-events.sql
  • packages/worker/src/app/account-billing-data.node.test.ts
  • packages/worker/src/app/account-billing-data.ts
  • packages/worker/src/app/account-retention-dispositions.ts
  • packages/worker/src/app/email-delivery-alerts.node.test.ts
  • packages/worker/src/app/email-delivery-alerts.ts
  • packages/worker/src/app/handlers/account-billing.ts
  • packages/worker/src/app/handlers/stripe-webhook.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/retention.node.test.ts
  • packages/worker/src/app/retention.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/billing/billing-config.node.test.ts
  • packages/worker/src/billing/billing-config.ts
  • packages/worker/src/billing/stripe-client.ts
  • packages/worker/src/billing/stripe-webhook-signature.node.test.ts
  • packages/worker/src/billing/stripe-webhook-signature.ts
  • packages/worker/src/billing/stripe-webhooks.ts
  • packages/worker/src/billing/stripe-webhooks.workers.test.ts
  • packages/worker/src/billing/subscription-sync.ts
  • packages/worker/src/billing/subscription-sync.workers.test.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/env-schema.ts
  • packages/worker/src/index.ts
  • packages/worker/src/index.workers.test.ts
  • tools/check-migrations.node.test.ts
  • tools/migration-ledger.json
📝 Walkthrough

Walkthrough

Changes

The PR adds Stripe webhook ingestion with signature verification, subscription synchronization, idempotency tracking, and 30-day retention. Billing pages expose subscription status and actions. It also adds an hourly email delivery burst alert with KV cooldown and administrator notifications.

Stripe billing synchronization

Layer / File(s) Summary
Stripe webhook ingress and processing
packages/worker/src/billing/stripe-webhooks.ts, packages/worker/src/billing/stripe-webhook-signature.ts, packages/worker/src/app/handlers/stripe-webhook.ts, packages/worker/src/app/router.ts, packages/worker/src/app/routes.ts, packages/worker/src/env-schema.ts, packages/worker/migrations/*, packages/worker/src/billing/*.test.ts, .github/workflows/deploy.yml, docs/contributing/...
Signed Stripe events are accepted at /webhooks/stripe, dispatched to billing synchronization, recorded for idempotency, and covered by configuration, deployment, and integration tests.
Subscription attribution and status contracts
packages/worker/src/billing/billing-config.ts, packages/worker/src/billing/subscription-sync.ts, packages/worker/src/billing/stripe-client.ts, packages/worker/src/app/handlers/account-billing.ts, packages/worker/src/billing/*.test.ts
Subscription results include status signals, checkout metadata carries stable-user attribution, and checkout linking resolves users through validated attribution fields.
Billing loader and status UI
packages/worker/src/app/account-billing-data.ts, packages/worker/src/app/loader-data.ts, packages/worker/client/routes/account-billing.tsx, packages/worker/src/app/account-billing-data.node.test.ts, docs/contributing/architecture/entitlements.md
Billing data exposes subscription status and usage navigation, while the billing page renders status badges, payment actions, portal access, and webhook freshness messaging.
Webhook event retention
packages/worker/src/app/retention.ts, packages/worker/src/app/account-retention-dispositions.ts, packages/worker/src/app/retention.node.test.ts, packages/worker/migrations/0093-stripe-webhook-events.sql, tools/migration-ledger.json, tools/check-migrations.node.test.ts, docs/contributing/architecture/data-storage.md
stripe_webhook_events is added to global 30-day retention, batched pruning, retention coverage, migration tracking, and tests.

Email delivery burst alerts

Layer / File(s) Summary
Email delivery burst detection and notification
packages/worker/src/app/email-delivery-alerts.ts, packages/worker/src/app/email-delivery-alerts.node.test.ts
Hourly checks count Cloudflare Email complaints and bounces, enforce thresholds and KV cooldowns, and notify administrators.
Scheduled alert lane and ownership
packages/worker/src/index.ts, packages/worker/src/index.workers.test.ts, docs/contributing/architecture/primitives.yaml, docs/contributing/security.md
The scheduled Worker invokes the alert lane at its UTC gate, with scheduling tests and updated primitive and security documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Stripe
  participant WorkerRoute
  participant WebhookProcessor
  participant BillingSync
  participant WebhookLedger
  Stripe->>WorkerRoute: POST signed webhook
  WorkerRoute->>WebhookProcessor: Verify raw request
  WebhookProcessor->>BillingSync: Process supported event
  BillingSync->>WebhookLedger: Record event id
  WorkerRoute-->>Stripe: Return success or retry status
Loading
sequenceDiagram
  participant Cron
  participant AlertChecker
  participant Database
  participant KV
  participant Administrators
  Cron->>AlertChecker: Run hourly lane
  AlertChecker->>Database: Count complaints and bounces
  AlertChecker->>KV: Check cooldown
  AlertChecker->>Administrators: Send alert when threshold is met
  AlertChecker->>KV: Store cooldown marker
Loading

Possibly related PRs

  • kentcdodds/kody#653: Established the retention-pruning framework extended here for Stripe webhook events.
  • kentcdodds/kody#839: Added migration filename validation related to the migration-prefix test updates here.
  • kentcdodds/kody#889: Hardened migration-ledger validation used by the new Stripe webhook migration entry.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR’s three main themes: Stripe webhooks, billing UX updates, and email delivery alerts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/stripe-webhooks-billing-alerts-0731

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 25, 2026 19:24
@github-actions

github-actions Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-940.kody-a99.workers.dev

Worker: kody-pr-940
D1: kody-pr-940-db
KV: kody-pr-940-oauth-kv

Mocks:

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/worker/src/billing/subscription-sync.ts (1)

22-31: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a friendly message for user_not_found.

resolveBillingErrorMessage() falls back to errorCode ?? trimmed, so when billingLink errors use new BillingLinkError('user_not_found', ...), this code and loadAccountBillingData({ errorCode: 'user_not_found' }) can render the raw string instead of a user-friendly message. Add user_not_found: ... to billingErrorMessages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/subscription-sync.ts` around lines 22 - 31, Add a
user-friendly `user_not_found` entry to the `billingErrorMessages` mapping used
by `resolveBillingErrorMessage()`, so both `BillingLinkError` instances and
`loadAccountBillingData({ errorCode: 'user_not_found' })` resolve to the
friendly message instead of the raw error code.
🧹 Nitpick comments (6)
packages/worker/src/billing/stripe-webhooks.workers.test.ts (1)

294-294: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move vi.unstubAllGlobals() into an afterEach so a failing assertion can't leak the fetch stub.

Both tests unstub as the last statement of the body; if any earlier expect throws, the stubbed fetch survives into the next test and produces cascading, misleading failures.

♻️ Proposed cleanup
-import { expect, test, vi } from 'vitest'
+import { afterEach, expect, test, vi } from 'vitest'
+afterEach(() => {
+	vi.unstubAllGlobals()
+})

Then drop the trailing vi.unstubAllGlobals() from both test bodies.

Also applies to: 362-362

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/stripe-webhooks.workers.test.ts` at line 294,
Move vi.unstubAllGlobals() into an afterEach hook covering both webhook tests,
then remove the trailing calls from each test body so cleanup runs even when
assertions fail.
packages/worker/src/billing/stripe-webhooks.ts (3)

329-334: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

record shadows the imported record schema helper.

Not a bug today — record() is only called at module scope (Lines 32, 48) — but any future use inside this function silently resolves to the string result. Rename the local.

♻️ Proposed rename
-	const record = await recordStripeWebhookEvent({
+	const recordResult = await recordStripeWebhookEvent({
 		env: input.env,
 		eventId: event.id,
 		eventType: event.type,
 		now: input.now,
 	})
-	if (record === 'duplicate') {
+	if (recordResult === 'duplicate') {
 		return { status: 200, body: { ok: true, duplicate: true } }
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/stripe-webhooks.ts` around lines 329 - 334,
Rename the local result of recordStripeWebhookEvent in the webhook handler to
avoid shadowing the imported record schema helper. Update all references to this
local value within the handler while preserving the existing module-scope
record() usage.

141-144: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

console.error for expected business outcomes will pollute error alerting.

customer_already_linked fires on every legitimate checkout replay, and invoice.payment_failed is a routine declined-card event, not a system error. Both are informational; demote to console.info/console.warn so genuine webhook failures stay signal.

Also applies to: 205-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/stripe-webhooks.ts` around lines 141 - 144,
Update the webhook logging for expected outcomes in the checkout-link handling
and invoice.payment_failed path to use console.info or console.warn instead of
console.error. Keep genuine webhook failure logging unchanged, and ensure both
customer_already_linked and routine payment-declined events no longer trigger
error alerting.

165-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated customer extraction; the parseSafe fallback is dead code.

Both handlers repeat the same block, and customerObjectSchema (all-optional) succeeds for any record input, so the readStringField fallback never runs. Collapse to a single helper.

♻️ Proposed dedupe
+function readEventCustomerId(object: Record<string, unknown>): string | null {
+	return readStringField(object, 'customer')
+}
+
 async function handleCustomerSubscriptionChange(input: {
 	env: Env
 	object: Record<string, unknown>
 	now?: Date
 }) {
-	const parsed = parseSafe(customerObjectSchema, input.object)
-	const customerId =
-		(parsed.success ? readCustomerId(parsed.value.customer) : null) ??
-		readStringField(input.object, 'customer')
+	const customerId = readEventCustomerId(input.object)
 	if (!customerId) {

Apply the same substitution in handleInvoicePaymentFailed, then drop customerObjectSchema and readCustomerId if unused elsewhere.

Also applies to: 188-195

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/stripe-webhooks.ts` around lines 165 - 172,
Deduplicate customer ID extraction in both handlers, including
handleInvoicePaymentFailed, by using one shared helper that reads the customer
field directly from the webhook object. Remove the redundant
parseSafe/customerObjectSchema path and delete customerObjectSchema and
readCustomerId if no longer referenced elsewhere; preserve the existing
missing-customer error and early return behavior.
packages/worker/src/billing/stripe-webhook-signature.ts (1)

11-12: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider relocating timingSafeEqualString to a shared crypto util.

A billing signature primitive importing from #worker/maintenance-handler.ts inverts the expected dependency direction and drags the maintenance module into this module's graph. Moving it next to toHex in @kody-internal/shared would keep the boundary clean.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/stripe-webhook-signature.ts` around lines 11 -
12, Move timingSafeEqualString from `#worker/maintenance-handler.ts` into the
shared crypto utilities alongside toHex, export it there, and update the billing
signature module to import it from `@kody-internal/shared`. Remove the
maintenance-module dependency while preserving the function’s existing behavior
and consumers.
packages/worker/src/billing/subscription-sync.ts (1)

99-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consolidate the three near-identical candidate lookups.

The stable-user-id, customer-id, and email lookups in resolveBillingUserForCheckoutLink are structurally identical (same query shape, same row mapping), differing only by the WHERE column. Extracting a small helper reduces copy/paste risk in this security-sensitive attribution path.

♻️ Proposed refactor
+async function findCandidateByColumn(
+	env: SyncEnv,
+	column: 'stable_user_id' | 'stripe_customer_id',
+	value: string,
+): Promise<BillingUser | null> {
+	const row = await env.APP_DB.prepare(
+		`SELECT id, email, stable_user_id FROM users WHERE ${column} = ?`,
+	)
+		.bind(value)
+		.first<{ id: number; email: string; stable_user_id: string }>()
+	return row
+		? { id: row.id, email: row.email, stableUserId: row.stable_user_id }
+		: null
+}
+
 	const stableUserIdHint = input.stableUserIdHint?.trim()
 	if (stableUserIdHint) {
-		const row = await input.env.APP_DB.prepare(
-			`SELECT id, email, stable_user_id FROM users WHERE stable_user_id = ?`,
-		)
-			.bind(stableUserIdHint)
-			.first<{ id: number; email: string; stable_user_id: string }>()
-		await pushCandidate(
-			row ? { id: row.id, email: row.email, stableUserId: row.stable_user_id } : null,
-		)
+		await pushCandidate(
+			await findCandidateByColumn(input.env, 'stable_user_id', stableUserIdHint),
+		)
 	}

(similarly for customerId; the email lookup stays separate since it uses lower(email))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/billing/subscription-sync.ts` around lines 99 - 183,
Refactor the repeated stable-user-id and customer-id query/mapping logic in
resolveBillingUserForCheckoutLink into a small reusable helper parameterized by
the WHERE column and value, then pass its result to pushCandidate. Keep the
email lookup separate because it uses lower(email), and preserve the existing
candidate ordering, deduplication, and validation behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/client/routes/account-billing.tsx`:
- Around line 481-501: Remove the click handler that unconditionally calls
preventDefault and window.location.assign from the manage-subscription anchor in
the showManageCta branch. Preserve the href={billingPortalPath} and existing
styling so normal clicks and modifier or middle-clicks use native link behavior.

In `@packages/worker/src/app/email-delivery-alerts.ts`:
- Around line 159-175: Update the email delivery alert flow around
sendCloudflareEmail to inspect its result before reporting success or writing
the six-hour cooldown marker. When the result is { ok: false, skipped: true },
return a non-notified status or throw so no notification marker is persisted;
preserve the existing success path, and add a regression test covering the
skipped result.
- Line 23: Scope the email delivery alert flow by threading the owning userId
through event queries, admin-recipient resolution, and cooldown reads/writes.
Update emailDeliveryAlertKvKey to include userId so each user has an independent
alert state and cooldown, ensuring no cross-user events, recipients, or
suppression state are shared.

---

Outside diff comments:
In `@packages/worker/src/billing/subscription-sync.ts`:
- Around line 22-31: Add a user-friendly `user_not_found` entry to the
`billingErrorMessages` mapping used by `resolveBillingErrorMessage()`, so both
`BillingLinkError` instances and `loadAccountBillingData({ errorCode:
'user_not_found' })` resolve to the friendly message instead of the raw error
code.

---

Nitpick comments:
In `@packages/worker/src/billing/stripe-webhook-signature.ts`:
- Around line 11-12: Move timingSafeEqualString from
`#worker/maintenance-handler.ts` into the shared crypto utilities alongside toHex,
export it there, and update the billing signature module to import it from
`@kody-internal/shared`. Remove the maintenance-module dependency while preserving
the function’s existing behavior and consumers.

In `@packages/worker/src/billing/stripe-webhooks.ts`:
- Around line 329-334: Rename the local result of recordStripeWebhookEvent in
the webhook handler to avoid shadowing the imported record schema helper. Update
all references to this local value within the handler while preserving the
existing module-scope record() usage.
- Around line 141-144: Update the webhook logging for expected outcomes in the
checkout-link handling and invoice.payment_failed path to use console.info or
console.warn instead of console.error. Keep genuine webhook failure logging
unchanged, and ensure both customer_already_linked and routine payment-declined
events no longer trigger error alerting.
- Around line 165-172: Deduplicate customer ID extraction in both handlers,
including handleInvoicePaymentFailed, by using one shared helper that reads the
customer field directly from the webhook object. Remove the redundant
parseSafe/customerObjectSchema path and delete customerObjectSchema and
readCustomerId if no longer referenced elsewhere; preserve the existing
missing-customer error and early return behavior.

In `@packages/worker/src/billing/stripe-webhooks.workers.test.ts`:
- Line 294: Move vi.unstubAllGlobals() into an afterEach hook covering both
webhook tests, then remove the trailing calls from each test body so cleanup
runs even when assertions fail.

In `@packages/worker/src/billing/subscription-sync.ts`:
- Around line 99-183: Refactor the repeated stable-user-id and customer-id
query/mapping logic in resolveBillingUserForCheckoutLink into a small reusable
helper parameterized by the WHERE column and value, then pass its result to
pushCandidate. Keep the email lookup separate because it uses lower(email), and
preserve the existing candidate ordering, deduplication, and validation
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3cac0260-9d55-43f4-9aec-d91ddd786b63

📥 Commits

Reviewing files that changed from the base of the PR and between 367cd15 and 33a46b4.

📒 Files selected for processing (36)
  • .github/workflows/deploy.yml
  • docs/contributing/architecture/data-storage.md
  • docs/contributing/architecture/entitlements.md
  • docs/contributing/architecture/primitives.yaml
  • docs/contributing/environment-variables.md
  • docs/contributing/security.md
  • docs/contributing/setup-manifest.md
  • packages/worker/.env.example
  • packages/worker/client/routes/account-billing.tsx
  • packages/worker/migrations/0093-stripe-webhook-events.sql
  • packages/worker/src/app/account-billing-data.node.test.ts
  • packages/worker/src/app/account-billing-data.ts
  • packages/worker/src/app/account-retention-dispositions.ts
  • packages/worker/src/app/email-delivery-alerts.node.test.ts
  • packages/worker/src/app/email-delivery-alerts.ts
  • packages/worker/src/app/handlers/account-billing.ts
  • packages/worker/src/app/handlers/stripe-webhook.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/retention.node.test.ts
  • packages/worker/src/app/retention.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/billing/billing-config.node.test.ts
  • packages/worker/src/billing/billing-config.ts
  • packages/worker/src/billing/stripe-client.ts
  • packages/worker/src/billing/stripe-webhook-signature.node.test.ts
  • packages/worker/src/billing/stripe-webhook-signature.ts
  • packages/worker/src/billing/stripe-webhooks.ts
  • packages/worker/src/billing/stripe-webhooks.workers.test.ts
  • packages/worker/src/billing/subscription-sync.ts
  • packages/worker/src/billing/subscription-sync.workers.test.ts
  • packages/worker/src/entitlements/test-schema.ts
  • packages/worker/src/env-schema.ts
  • packages/worker/src/index.ts
  • packages/worker/src/index.workers.test.ts
  • tools/migration-ledger.json

Comment thread packages/worker/client/routes/account-billing.tsx
export const emailDeliveryAlertThreshold = 20
/** Avoid re-paging on the same sustained spike every hour. */
export const emailDeliveryAlertCooldownMinutes = 6 * 60
export const emailDeliveryAlertKvKey = 'ops-alert:email-delivery-burst:v1'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Scope this alert lane and its cooldown by userId.

This aggregates all users’ delivery events, resolves all admin recipients, and uses one shared cooldown key. A burst from one user can disclose/suppress alert state for unrelated users. Thread an owner userId through this lane, apply it to the event and recipient queries, and namespace the KV key by that ID.

As per coding guidelines, “every read/write path must be scoped by userId” and “Cross-user data sharing is a bug.”

Also applies to: 72-79, 85-96, 138-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/email-delivery-alerts.ts` at line 23, Scope the email
delivery alert flow by threading the owning userId through event queries,
admin-recipient resolution, and cooldown reads/writes. Update
emailDeliveryAlertKvKey to include userId so each user has an independent alert
state and cooldown, ensuring no cross-user events, recipients, or suppression
state are shared.

Source: Coding guidelines

Comment thread packages/worker/src/app/email-delivery-alerts.ts Outdated
Comment thread packages/worker/client/routes/account-billing.tsx
cursoragent and others added 7 commits July 25, 2026 20:11
POST /webhooks/stripe verifies Stripe-Signature, idempotently records
event ids, and reuses checkout link + plan refresh helpers so customer
linking no longer depends on the success redirect alone. Cron poll stays
as backup; resolveSubscriptionPlan now returns subscriptionStatus for UX.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Mirror the auth-denial ops alert: count Cloudflare Email Sending
complained/bounced/failed/rejected outcomes over 60 minutes, page
admins at threshold 20 with a 6h KV cooldown, and document the lane.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Pass Stripe subscriptionStatus through billing loader data, surface status
badges and past_due guidance, add Subscribe/Manage/Usage CTAs, and fix
entitlements docs to match always-on billing page refresh.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Process Stripe webhooks before recording event ids so failures do not ack
concurrent duplicates; count only complained/bounced for delivery alerts;
retain stripe_webhook_events for 30 days.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@cursor
cursor Bot force-pushed the cursor/stripe-webhooks-billing-alerts-0731 branch from e6476db to 52ca450 Compare July 25, 2026 20:11
Stripe only maps active/trialing subscriptions to stripe_plan, so past_due
and unpaid already drop paid access — say restore, not keep.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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 using default effort 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.

Reviewed by Cursor Bugbot for commit 9184c6d. Configure here.

})}
>
Manage subscription
</a>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Portal link breaks SPA navigation

Medium Severity

The new Manage subscription anchor uses a plain href to /account/billing/portal, but the global click handler intercepts same-origin links and SPA-navigates. That path has no client loader or route—only a server GET that 302s to Stripe—so a normal click updates the URL without a full document request and never opens the billing portal.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9184c6d. Configure here.

@kody-bot
kody-bot merged commit b88d025 into main Jul 25, 2026
5 checks passed
@kody-bot
kody-bot deleted the cursor/stripe-webhooks-billing-alerts-0731 branch July 25, 2026 20:20
cursor Bot pushed a commit that referenced this pull request Jul 26, 2026
Fold post-#937/#939/#940 account verification tests into fewer workflow
journeys and drop low-signal Playwright coverage.

- account-values handler: list/select/save/delete + rejection matrix
- account-memories handler: list/filter/select + soft/force delete
- activity-data helpers + load/cursor; billing refresh workflow without
  instructional error-copy pins
- auth-session password-change matrix; auth-denial and email-delivery
  alert cron/threshold/cooldown journeys
- trim activity e2e instructional empty-state copy; drop memories e2e
  (values e2e + memories handler cover the pattern); drop jobs schedule
  text pin

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
kody-bot pushed a commit that referenced this pull request Jul 26, 2026
…#958)

* test: consolidate low-signal workers tests into workflow journeys

Collapses 32 tests across four files down to 15, following the
'fewer longer workflow tests' principle from testing-principles.md.

run-records.workers.test.ts (20 → 9):
- Merge eager-begin/finish, finish-only upsert, one-shot recordRunRecord,
  waitUntil non-blocking, and execute on-failure policy into a single
  'write surfaces journey' test.
- Delete 'sandbox level markers become structured log levels' — covered
  by the existing sandbox e2e test which already asserts structured
  log levels on each entry.
- Merge retention-priority, cap-protect-running, stale-reconcile,
  stale-cap-evict, and age-cap scenarios into 'cap and stale retention
  journey' (each with its own userId for DO isolation).
- Extend 'write after idle re-arms alarm and age-prunes' into a full
  'alarm lifecycle' narrative that also covers the fresh-arm far-future
  deadline assertion and the self-termination assertion from the two
  standalone alarm tests.

runs.workers.test.ts (5 → 2):
- Keep the auth guard test unchanged.
- Merge run_list filter smoke, run_get with logs/foreign/missing
  rejection, run_summary counts, and run_list/run_summary tenant
  isolation into a single 'run capabilities smoke' test. Filter/
  pagination matrix remains in run-records.workers.test.ts.

execute-console-capture.workers.test.ts (4 → 2):
- Merge success-levels, throw-capture, and unshimmed-methods into
  'console capture contract'; keep 'reused dynamic workers' separate.

stripe-webhooks.workers.test.ts (3 → 2):
- Fold the 503-when-secret-unset guard into the main journey test as
  an upfront check before any seeding.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

* test: fold Sentry, D1 retry, and small unit microtests into journeys

Collapse post-#934 one-string-per-filter and sibling unit cases toward
fewer longer workflow tests.

- sentry-options: fold UserCodeError hint + platform keep into the main
  filter journey; delete standalone user-code-error.node.test.ts
- d1-retry: keep representative matchers and one retry/fail path
- observability: fold conversationId/storageId/detail context into the
  platform-bug reporting journey
- activation: merge twice-same-package, HTTP ignore, sticky timestamp,
  fast path, and never-throw degradation
- fold stripe signature fixture, package-workflow sentry tags,
  invocation surface/name helpers, and package_service_states count +
  entitlement boundary

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

* test: consolidate account list/detail and alert suites

Fold post-#937/#939/#940 account verification tests into fewer workflow
journeys and drop low-signal Playwright coverage.

- account-values handler: list/select/save/delete + rejection matrix
- account-memories handler: list/filter/select + soft/force delete
- activity-data helpers + load/cursor; billing refresh workflow without
  instructional error-copy pins
- auth-session password-change matrix; auth-denial and email-delivery
  alert cron/threshold/cooldown journeys
- trim activity e2e instructional empty-state copy; drop memories e2e
  (values e2e + memories handler cover the pattern); drop jobs schedule
  text pin

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

* style: apply oxfmt after mistaken prettier pass

CI format:check uses oxfmt (tabs/single quotes). A Prettier pass had
rewritten touched test files to spaces/double quotes.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
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.

3 participants