ASAASA Standard
Active Phase 1Production Foundation

Billing State Integrity

Billing Safety · BIL-09 · BIL-05, BIL-06, BIL-07, BIL-11, BIL-20, BIL-22 · Priority: P1

Why This Cluster Matters

Billing state integrity means your app's understanding of "who is paying for what" matches Stripe's reality — at all times. When billing state drifts, users get free access to paid features, paying customers lose access they've paid for, or subscriptions enter impossible states that break your revenue.

AI code generators build the happy path: user clicks "Subscribe" → Stripe charges → app grants access. They skip the 90% of billing logic that handles edge cases: subscription changes, failed payments, grace periods, cancellations, reactivations, and the dozens of Stripe webhook events that modify state.

This cluster covers 7 checks that together verify your billing state machine is complete and consistent.


Checks in This Cluster

ID Check Priority
BIL-05 Subscription state machine P1
BIL-06 Entitlement/plan limit checking P1
BIL-07 Customer ↔ User sync P1
BIL-09 No client-side billing state P1
BIL-11 Cancellation handling P1
BIL-20 Portal session auth P2
BIL-22 Trial period handling P2

BIL-05: Subscription State Machine

Your app must handle every subscription state transition Stripe can produce: active → past_due → canceled, active → paused, trialing → active, active → incomplete. AI tools typically handle only active and ignore all other states — meaning a user whose payment fails remains on the paid plan indefinitely.

What to verify: Your app correctly maps Stripe subscription statuses (active, past_due, canceled, incomplete, trialing, paused, unpaid) to entitlement decisions.


BIL-06: Entitlement/Plan Limit Checking

When a user is on the "Starter" plan with 100 API calls/month, does your app actually enforce that limit? AI tools generate plan selection UI but rarely implement server-side enforcement of plan limits. The check runs on the client — meaning users can bypass limits with a simple API call.

What to verify: Plan limits are enforced server-side, checked before every restricted action, and synced with the actual Stripe subscription.


BIL-07: Customer ↔ User Sync

Every Supabase user must map to exactly one Stripe customer. AI tools sometimes create duplicate Stripe customers for the same user (on every checkout), or fail to link the Stripe customer ID back to the user record — breaking subscription lookups.

What to verify: One-to-one mapping between auth.users.id and stripe_customer_id. No orphaned customers, no duplicates.


BIL-09: No Client-Side Billing State

If your app checks subscription status in a React component using state from localStorage or a context provider, any user can modify that state to grant themselves premium access. Billing state must be read from the server on every protected request.

What to verify: Entitlement decisions happen server-side. The client receives the result ("you have access" / "you don't"), not the billing data to evaluate.


BIL-11: Cancellation Handling

When a user cancels, does your app: (a) immediately revoke access, (b) maintain access until the end of the billing period, or (c) do nothing because the webhook isn't handled? AI tools typically don't handle customer.subscription.deleted or customer.subscription.updated with cancel_at_period_end.

What to verify: Cancellation respects the billing period end date. Access is revoked at the right time — not immediately and not never.


BIL-20: Portal Session Auth

Stripe's Customer Portal lets users manage their subscription. The portal session must be created server-side with the authenticated user's Stripe customer ID. If the customer ID comes from the client, a user can access another customer's portal.

What to verify: Portal session creation is server-side, uses the authenticated user's stored stripe_customer_id, and never accepts a customer ID from the client.


BIL-22: Trial Period Handling

Free trials must transition cleanly to paid subscriptions or cancel. AI tools often create trials that never expire (no webhook handler for customer.subscription.trial_will_end) or that grant permanent access after trial end because the state check only looks at status === 'active' and ignores trialing.

What to verify: Trial end is handled. Users are either converted to paid or lose access when the trial expires.


References

Related Checks


Is your app safe? Run Free Scan →