Skip to main content
One idea at a time

Store each provider event once

Stripe sends event evt_example, and Taskboard commits its inbox record. The response disappears before reaching Stripe. Stripe may send the same event again. Receiving it twice does not mean the subscription changed twice.

A webhook inbox uses the provider event ID as a unique identity. The transaction inserts the event and schedules work only when the event is newly accepted.

Illustrative SQL: one inbox row per event
INSERT INTO webhook_events (provider_event_id, payload)
VALUES ($1, $2)
ON CONFLICT (provider_event_id) DO NOTHING;

The handler must inspect whether it inserted a row. Scheduling a new job unconditionally after DO NOTHING would deduplicate storage while duplicating work.

A duplicate is an accepted delivery​

An already-recorded valid event can receive a success response. Taskboard has already preserved the event's obligation. Returning a failure for every duplicate would cause repeated delivery without improving correctness.

The event insert and job insert belong in the same transaction. If Taskboard stores the event, then stops before scheduling work, a later duplicate might otherwise be ignored forever.

Deduplication does not establish order​

Two different event IDs can describe subscription changes in an unexpected arrival order. An older update may arrive after a cancellation. Applying every snapshot directly to the plan can accidentally restore access.

Taskboard uses accepted events to schedule reconciliation with the provider's current subscription state. The inbox prevents repeated acceptance of one delivery identity. Reconciliation handles the separate question of what the subscription means now. Stripe documents that webhook delivery order is not guaranteed. Stripe event ordering.

Different events can also refer to the same subscription. The worker must coordinate updates to that subscription or workspace. Event-level deduplication alone does not protect against overlapping remote reads finishing in the wrong order.

This is why the app keeps separate records for incoming events, jobs, and current subscription state. Each record answers a different question: what arrived, what work remains, and what access the workspace currently has.

Read inbox acceptance in Read api/src/billing.ts and the unique constraint in Read api/src/schema.ts.

Take away

Deduplicate by provider event ID inside the scheduling transaction. Handle out-of-order changes through a separate current-state reconciliation rule.

Does a unique event ID prevent an older event from undoing a newer update?

No. The two updates can have different IDs. Uniqueness prevents repeated acceptance of one event; it does not prove chronological ordering.