Two systems can disagree temporarily
Stripe creates a checkout session, then Taskboard loses its connection before receiving the result. Stripe knows about the session. Taskboard may not yet have its URL. That disagreement is a normal failure case, not evidence that Stripe did nothing.
There is no ordinary Postgres transaction that commits a row and a Stripe API request together. Taskboard therefore needs an explicit recovery strategy for each direction of the disagreement.
Preserve the intended operation
The checkout request records its operation identity. Calls to Stripe use a stable provider idempotency key derived from that identity. If Taskboard retries after a timeout or restart, it uses the same provider key.
Taskboard records checkout intent K.
Stripe creates session S for provider key K.
The response is lost.
Taskboard retries with provider key K.
Taskboard recovers the result for S.
Provider idempotency has its own scope, retention, and request-matching rules. Taskboard must work within those rules rather than assuming keys last forever. The provider key also needs to distinguish different provider operations belonging to one local request.
Reconcile authoritative state
Subscriptions introduce a different disagreement. Stripe may change a subscription before Taskboard processes its webhook. The local plan then temporarily lags behind the provider.
Reconciliation means fetching the provider's current subscription state and translating it into Taskboard's local billing record. A saved webhook schedules that work. The implementation must coordinate concurrent reconciliations so an older fetch cannot overwrite a newer result.
Event timestamps alone do not solve every race. An event can arrive out of order, and a remote read can finish after another read. Serialized work or fenced writes give the database a rule for accepting the result.
The product also needs a policy during uncertainty. Taskboard should expose a pending state rather than claim payment success from a browser redirect. Recovery may require another provider read or operator inspection. Exactly-once language hides these unresolved boundaries.
Follow checkout and reconciliation in Read api/src/billing.ts and durable execution in Read api/src/jobs.ts.
Store intent, reuse provider operation keys, and reconcile current state. A local transaction cannot make two independent services commit together.
Why not roll back Stripe when the local database fails?
A compensating provider request can also fail, and some actions cannot be fully reversed. Recovery must describe that request as another fallible operation, not as a database rollback.