Skip to content

fix(billing): converge Stripe subscription syncs and stop webhook echoes overwriting unsynced changes - #8764

Merged
waleedlatif1 merged 14 commits into
stagingfrom
fix/stripe-sync-convergence
Oct 8, 2026
Merged

waleedlatif1 merged 14 commits into
stagingfrom
fix/stripe-sync-convergence

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

Sim syncs cancel_at_period_end, Team seats and the customer contact from the DB to Stripe through outbox events, while the Better Auth Stripe plugin copies cancel_at_period_end/seats from every customer.subscription.updated back into the DB unconditionally. Five real convergence bugs followed — each reproduced against unmodified staging code first (real Postgres, the real plugin driven over HTTP, a fake Stripe that controls landing order):

  • Concurrent cancel syncs for one subscription (overlapping outbox runs) could leave Stripe holding the older value
  • Webhook echoes — a delayed, out-of-order, or unrelated customer.subscription.updated overwrote a DB change Sim had committed but not yet pushed, and the pending sync then pushed the overwritten value (no outbox concurrency needed); a lost race was echoed back into the DB, so both systems agreed on the wrong value
  • Idempotency key reuse — a retry after a partial failure reused outbox:<eventId> with a different value and Stripe rejected it until dead-letter
  • Customer contact sync had the same race as cancel sync
  • Seats — a stale webhook wiped an unsynced seat change

Fix

  • Handlers converge: cancel and contact syncs read the DB, retrieve Stripe, write only if different (fresh key per write), re-read the DB and loop, then throw to retry — the pattern the seat sync already used
  • Committed values are recorded: every non-Stripe writer of cancelAtPeriodEnd/seats records the value under the subscription row lock with a DB-clock timestamp onto every pending/processing/dead-lettered sync for that subscription (lib/billing/webhooks/subscription-sync.ts), so no event can carry a stale value — including requeued dead letters, Team activation, and Better Auth's /subscription/restore (via the existing hooks.after)
  • Echo guard in onEvent after the plugin's write: while a Sim sync is in flight its latest committed value is restored; a change made in Stripe itself (portal, dashboard, Better Auth endpoints — detected by Sim's idempotency-key prefix) wins and supersedes in-flight values; with nothing in flight the live Stripe value is persisted (never the event payload), so out-of-order delivery can't regress it. Events from an older deploy leave the field as today
  • Lock order is uniform and documented in one place: org mutation lock → subscription row → outbox rows (operator retry paths fixed to match)

Type of Change

  • Bug fix

Testing

  • stripe-sync-convergence.integration.ts (22 tests, real Postgres + the real Better Auth Stripe plugin over HTTP): each bug, every revival path (portal change then unrelated webhook, requeued dead letter, slow in-flight sync, enterprise follow-up retry, legacy deploy events, customer restore), Team-activation race, operator-retry deadlocks (deterministic lock interleaving), and a plan check that no reconcile query scans settled rows. Every test failed before its fix; every guard shown red when reverted
  • bun run test:integration billing + outbox + auth adapter suites; root bun run test; bun run lint, bun run type-check, bun run check:audits, bun run docs-manifest:check
  • Several independent adversarial safety reviews; findings fixed in follow-up commits on this branch

Known, intentional: a Stripe-dashboard seat edit made while a seat sync is in flight loses to Sim (Team seats follow the member count); cancelAt/canceledAt stay as the plugin writes them.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing (new tests pass the test-audit authoring gate)
  • 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 Oct 7, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Oct 8, 2026 12:14am UTC

Request Review

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

We detected this is a high-risk PR and are running a free ultrareview. An ultrareview is a deeper, multi-pass review that catches hard-to-find bugs a standard review can miss. We'll post the findings when it completes.

This PR appears to change concurrency-sensitive code such as locks, queues, or retries, where a missed race may only surface under production load, so a deeper multi-pass review is worth running.

Want an ultrareview on every high-risk PR? Set up automated ultrareviews.

@greptile-apps

greptile-apps Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Critical risk] Overhauls Stripe subscription sync and webhook handling for billing.

The PR appears safe to merge; no new actionable issue was found.

Summary

This PR records committed cancellation and seat values on retryable events. Workers reread those values, and the webhook guard repairs stale Stripe echoes without losing pending changes.

  • Since the last review, only latestOutboxEventId changed. It now keeps timestamp precision and rejects tied timestamps instead of choosing arbitrarily.
  • The worker, retry, membership, and restore-test fixes address the earlier unnumbered findings. The assertions on subscription-sync mock calls were removed.
  • waleedlatif1 accepted arrival-order precedence for changes made in Stripe because comparing clocks could override a newer customer action. Greptile accepted that explanation.
  • waleedlatif1 accepted the restore after-hook failure risk because Stripe has already applied the restore, retrying the endpoint is refused, and the codebase avoids app-level transaction retries. The failure remains logged.
  • The PR also intentionally lets Sim’s pending seat count override a Stripe-dashboard seat edit.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Writer[Sim commits a billing change] --> Lock[Lock subscription row]
  Lock --> Record[Update values on retryable events]
  Record --> Worker[Worker rereads its recorded value]
  Worker --> Stripe[Read Stripe and write only if different]
  Stripe --> Recheck[Recheck the committed value]
  Recheck -->|Changed| Worker
  Recheck -->|Unchanged| Complete[Complete event]
  Stripe --> Webhook[Stripe webhook]
  Webhook --> Plugin[Plugin writes event values]
  Plugin --> Guard[Restore pending Sim values or accept live Stripe]
Loading

Reviews (7) · Last reviewed commit: "test(billing): latest-sync helper fails ..." · Reviewed by Greptile

Comment thread apps/sim/lib/auth/auth.ts
Comment thread apps/sim/lib/billing/webhooks/subscription-sync.ts Outdated
Comment thread apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts Outdated
Comment thread apps/sim/lib/admin/subscription-lifecycle.test.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

We detected this is a high-risk PR and ran a free ultrareview. An ultrareview is a deeper, multi-pass review that catches hard-to-find bugs a standard review can miss.

This PR appears to change concurrency-sensitive code such as locks, queues, or retries, where a missed race may only surface under production load, so a deeper multi-pass review is worth running.

Want an ultrareview on every high-risk PR? Set up automated ultrareviews.

2 issues found across 22 files

Confidence score: 3/5

  • In apps/sim/lib/auth/auth.ts, if the DB/outbox transaction fails after Stripe restores a subscription, the endpoint still succeeds and an older cancellation sync can re-cancel it. Keep the restore bookkeeping durable or make the failure retryable.
  • In packages/testing/src/mocks/stripe.mock.ts, failNextRequest is documented as failing after the update, but it fails before processing. Move that description to failNextUpdateAfterApplying.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/testing/src/mocks/stripe.mock.ts">

<violation number="1" location="packages/testing/src/mocks/stripe.mock.ts:343">
P3: `failNextRequest` is documented as applying the update before failing, but it fails before processing; move that description above `failNextUpdateAfterApplying`.</violation>
</file>

<file name="apps/sim/lib/auth/auth.ts">

<violation number="1" location="apps/sim/lib/auth/auth.ts:1110">
P1: This catch makes restore bookkeeping best-effort: if the DB/outbox transaction fails after Stripe has restored the subscription, the endpoint still succeeds and an older cancellation sync can later re-cancel it. Rethrow the error so the idempotent restore can be retried, or enqueue a durable recovery job.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/sim/lib/auth/auth.ts Outdated
Comment thread apps/sim/lib/auth/auth.ts
Comment thread apps/sim/lib/billing/webhooks/subscription-sync.ts
Comment thread apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts Outdated
Comment thread apps/sim/lib/core/outbox/service.ts Outdated
Comment thread apps/sim/lib/billing/organizations/provision-seat.ts Outdated
Comment thread packages/testing/src/mocks/stripe.mock.ts Outdated
Comment thread apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts Outdated
@waleedlatif1
waleedlatif1 force-pushed the fix/stripe-sync-convergence branch from 5e44efe to 26f7bc6 Compare October 7, 2026 21:53
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 22 files

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/sim/lib/billing/webhooks/outbox-handlers.ts
Comment thread apps/sim/lib/billing/webhooks/subscription-sync.ts Outdated
Comment thread apps/sim/lib/billing/webhooks/subscription-sync.ts Outdated
@waleedlatif1
waleedlatif1 force-pushed the fix/stripe-sync-convergence branch from 26f7bc6 to 2bd650a Compare October 7, 2026 22:40
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 22 files

Reply with feedback, questions, or to request a fix.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/sim/lib/billing/webhooks/outbox-handlers.ts
Comment thread apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts Outdated
Comment thread packages/testing/src/mocks/stripe.mock.ts Outdated
Comment thread apps/sim/lib/billing/webhooks/subscription-sync.ts
@waleedlatif1
waleedlatif1 force-pushed the fix/stripe-sync-convergence branch from 2bd650a to 39f069b Compare October 7, 2026 23:05
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

Comment thread apps/sim/lib/billing/webhooks/outbox-handlers.ts

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 22 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1
waleedlatif1 force-pushed the fix/stripe-sync-convergence branch from 39f069b to 4f1c786 Compare October 7, 2026 23:41
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 22 files

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Turn on auto-fix | Re-trigger cubic

Comment thread apps/sim/lib/billing/webhooks/stripe-sync-convergence.integration.ts Outdated
Comment thread apps/sim/lib/billing/organizations/seats.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 22 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 22 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Turn on auto-fix | Re-trigger cubic

@waleedlatif1
waleedlatif1 merged commit 886cfa2 into staging Oct 8, 2026
47 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/stripe-sync-convergence branch October 8, 2026 00:41

This branch was previously deployed

1 inactive deployment
Preview — 9cccfb44 Deployed Oct 8, 2026 by vercel[bot]
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