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.
ACTIVE_WITHOUT_ACCESS
Paddle subscription is entitled, but local access is disabled.
PRODUCT_DRIFT
The local product does not match the current Paddle product.
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
Then configure the mapping and run a check:
paddle-integrity configure --environment sandbox
paddle-integrity doctor --environment sandbox
paddle-integrity scan --environment sandbox
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)
“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:
subscription.canceledlands, you re-open access you just closed. Store Paddle’soccurred_at/updated_atand only apply a change that is newer than the state you hold, or your reconciler and the live webhook path fight each other.statusis itself an authorization input. If the app reads entitlement from asubscriptionsrow 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 toactive, 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...