Data Model: User vs Customer
Never mix Auth data with Billing data. Keep them loosely coupled.
🔑 User Table (App DB)
user_id(UUID)emailstripe_customer_id(Foreign Key)
💳 Payment Gateway (Stripe)
CustomerobjectPaymentMethods(Tokens)Subscriptions
Subscription Lifecycle (State Machine)
Billing logic is complex. Model it as a Finite State Machine (FSM) to handle transitions deterministically.
Trialing
→
Active (Paid)
⬇ Payment Fails
Past Due
→
Canceled
OR
Unpaid
- Active: Payment successful. Grant feature access.
- Past Due: Payment failed (insufficient funds). Enter Dunning (retry logic). Do NOT revoke access yet.
- Canceled/Unpaid: Retries exhausted (e.g., after 14 days). Revoke access.
Idempotency & Webhooks
Payment gateways send webhooks (e.g., invoice.paid) asynchronously. Network issues can cause
duplicate events. Your handler MUST be Idempotent.
Rule: Never process the same payment event twice. It could result in double provisioning or
false analytics.
# Python (Flask) Example: Handling Stripe Webhooks safely
import stripe
from flask import Flask, request, jsonify
@app.route('/webhook', methods=['POST'])
def stripe_webhook():
payload = request.data
sig_header = request.headers['Stripe-Signature']
event = None
try:
# 1. Verify Signature (Security)
event = stripe.Webhook.construct_event(payload, sig_header, endpoint_secret)
except ValueError:
return 'Invalid payload', 400
# 2. Idempotency Check (Redis/DB)
event_id = event['id']
if redis.exists(f"processed_event:{event_id}"):
return jsonify({'status': 'ignored - duplicate'}), 200
# 3. Process Event based on Type
if event['type'] == 'invoice.payment_succeeded':
handle_payment_success(event['data']['object'])
# 4. Mark as Processed (TTL 24h)
redis.set(f"processed_event:{event_id}", "true", ex=86400)
return jsonify({'status': 'success'}), 200
PCI Compliance (SAQ A)
Never let raw Credit Card numbers touch your servers. If they hit your API logs, you are liable.
| Concept | Best Practice |
|---|---|
| Tokenization | Frontend sends Card Data directly to Stripe (Stripe.js). Stripe returns a safe token
(e.g., `tok_123`). Your backend only saves the token. |
| Stripe Elements | Using hosted inputs in `iframe` ensures card data never touches your DOM, reducing compliance scope to SAQ A (the easiest level). |
Summary
- State Machines: Essential for managing subscription status (`active`, `past_due`, `canceled`).
- Dunning: Automation for recovering failed payments (smart retries).
- Idempotency: Treat every webhook as if it could be delivered 5 times.
- Compliance: Offload all sensitive data handling to the Payment Processor (Tokenization).