DEV Community

Shimaji
Shimaji

Posted on

Subscription webhooks aren't enough: detecting drift between Paddle and Supabase

Building subscription billing is usually straightforward until the billing provider and your application database stop agreeing.

I've been testing a Paddle + Supabase setup, and I became interested in a specific problem:

How do you know that your application's access state still matches Paddle after the webhook has been processed?

A successful checkout or webhook delivery doesn't necessarily prove that the two systems remain consistent over time.

For example, I wanted to be able to detect cases like:

  • Paddle says a subscription is active, but local access is disabled
  • Paddle says a subscription is canceled or paused, but access is still enabled
  • A subscription exists in Paddle but is missing from the local database
  • Product or price IDs differ between Paddle and the application
  • Customer mappings are missing
  • A very recent Paddle change is still propagating and should not immediately be treated as an error

These aren't necessarily billing-provider bugs.

They can happen because the application has its own state, webhook processing can fail, database updates can be incomplete, or the two systems simply change at different times.

A reconciliation check

I ended up building a small CLI that compares the current Paddle subscription state with the corresponding records in Supabase.

It's called Paddle Integrity CLI.

The idea is deliberately simple:

Detect → Explain → Fix → Verify

It is read-only.

It does not modify Paddle, update Supabase rows, provision access, or automatically repair anything.

Instead, it reports differences so that you can decide what should be corrected.

Some examples:

CANCELED_STILL_ACTIVE
Paddle subscription is canceled, but local access is still enabled.
Enter fullscreen mode Exit fullscreen mode
ACTIVE_WITHOUT_ACCESS
Paddle subscription is entitled, but local access is disabled.
Enter fullscreen mode Exit fullscreen mode
PRODUCT_DRIFT
The local product does not match the current Paddle product.
Enter fullscreen mode Exit fullscreen mode

Recent changes can also be reported as Pending rather than immediately being treated as failures.

That matters because I wanted the checker to avoid turning normal propagation delays into false alarms.

Running it

The package is available on npm:

npm install --global paddle-integrity
Enter fullscreen mode Exit fullscreen mode

Then configure the mapping and run a check:

paddle-integrity configure --environment sandbox
paddle-integrity doctor --environment sandbox
paddle-integrity scan --environment sandbox
Enter fullscreen mode Exit fullscreen mode

I strongly recommend starting with Paddle Sandbox.

The scanner uses read-only provider requests.

Privacy

Telemetry is OFF by default.

If you don't explicitly opt in, no anonymous usage telemetry is sent.

The CLI also avoids putting credentials into its configuration file.

Why I built it

What I'm really trying to validate is whether subscription drift is a recurring operational problem for small SaaS teams using Paddle + Supabase.

The CLI itself is free and open source:

https://github.com/shimaji/paddle-integrity

npm:

https://www.npmjs.com/package/paddle-integrity

I'm also exploring whether continuous hosted monitoring would eventually be useful, but I don't want to build that until there is evidence that people actually experience this problem.

So I'm especially interested in feedback from anyone already running subscription billing in production:

Have you encountered cases where your billing provider and your application's access state became inconsistent?

And if so, what kinds of drift were the hardest to detect?

Top comments (1)

Collapse
 
cekuu35 profile image
Cenk KURTOĞLU •

“A successful webhook delivery doesn’t prove the two systems stay consistent” is the whole point, and a read-only reconciler is the right first step. Two additions that make the fix step safe rather than just the detection:

  1. Order writes by the provider’s clock, not arrival. Webhooks arrive out of order; if you reconcile to “current” and then a late subscription.canceled lands, you re-open access you just closed. Store Paddle’s occurred_at/updated_at and only apply a change that is newer than the state you hold, or your reconciler and the live webhook path fight each other.
  2. The reconciled status is itself an authorization input. If the app reads entitlement from a subscriptions row with the anon/client key, turn RLS on with a read-own policy and no client update path — otherwise the drift you are carefully detecting can also be self-inflicted: a user PATCHes their own row to active, and the reconciler then “confirms” the wrong state.

A drift check is only as trustworthy as the table it reads. A two-account probe for that “subscriptions row must not be client-writable” shape: github.com/cekuu35/supabase-rls-le...