10 KiB
Stripe Billing & Subscriptions – Developer Guide
This document explains how Stripe is integrated into ALwrity for subscriptions, billing, disputes, and fraud handling. It is aimed at developers working on the backend and frontend.
1. High-Level Architecture
- Backend
- Core service:
StripeService- File:
backend/services/subscription/stripe_service.py
- File:
- Subscription/payment API routes:
backend/api/subscription/routes/payment.pybackend/api/subscription/routes/disputes.pybackend/api/subscription/routes/fraud_warnings.py
- Models:
UserSubscription,SubscriptionPlan,BillingCycle,UsageStatus,FraudWarning- File:
backend/models/subscription_models.py
- File:
- Core service:
- Frontend
- Pricing and checkout UI:
frontend/src/components/Pricing/PricingPage.tsx
- Internal admin dashboards:
frontend/src/pages/StripeDisputesDashboard.tsx
- Routing:
frontend/src/App.tsx(route at/stripe-disputes)
- Pricing and checkout UI:
Data flows:
- Public users:
- Browse pricing → select plan → start Stripe Checkout → complete subscription.
- Admin/internal users:
- Use
/stripe-disputesdashboard to manage disputes and early fraud warnings.
- Use
2. Configuration & Environment
Required environment variables (backend):
STRIPE_SECRET_KEY- Stripe API key (test or live).
STRIPE_WEBHOOK_SECRET- Webhook signing secret for subscription webhooks.
STRIPE_MODE- Stripe mode selector (
testorlive). If unset, mode is inferred fromSTRIPE_SECRET_KEYprefix.
- Stripe mode selector (
STRIPE_PLAN_PRICE_MAPPING_TEST- JSON map for test mode tier/cycle to Stripe price IDs.
STRIPE_PLAN_PRICE_MAPPING_LIVE- JSON map for live mode tier/cycle to Stripe price IDs.
STRIPE_PLAN_PRICE_MAPPING(fallback)- Optional shared JSON map used only if mode-specific mapping env vars are not set.
ADMIN_EMAILS(optional)- Comma-separated list of admin emails allowed to access dispute/fraud endpoints.
ADMIN_EMAIL_DOMAIN(optional)- Domain considered admin (e.g.
example.com).
- Domain considered admin (e.g.
DISABLE_AUTH(optional)- If
"true", bypasses admin checks for local/testing use only.
- If
Stripe configuration:
- Price IDs are loaded from environment JSON and validated at backend startup.
- Required mapping keys (fail-fast):
basic.monthly,pro.monthly. - Mode-specific env vars allow separate test/live values with no code edits.
- Webhook endpoint must be configured in Stripe Dashboard:
- Path:
/api/subscription/webhook - Events:
checkout.session.completed,invoice.payment_succeeded,invoice.payment_failed,customer.subscription.updated,customer.subscription.deleted,radar.early_fraud_warning.created(and optionallyradar.early_fraud_warning.updated).
- Path:
3. Plans, Prices and Mapping
Stripe price mapping is loaded in StripeService from env JSON:
- File:
backend/services/subscription/stripe_service.py
Key structures:
STRIPE_PLAN_PRICE_MAPPING- Runtime map of
(SubscriptionTier, BillingCycle)→ Stripeprice_idparsed from env vars.
- Runtime map of
STRIPE_PRICE_TO_PLAN- Reverse map:
price_id→{ tier, billing_cycle }.
- Reverse map:
Helper methods:
_get_price_id_for_plan(tier, billing_cycle) -> str- Used when creating Checkout sessions.
_get_plan_for_price_id(price_id) -> (SubscriptionPlan, BillingCycle)- Used when mapping Stripe subscription items back into our internal
SubscriptionPlan.
- Used when mapping Stripe subscription items back into our internal
Adding or updating plans
- Create prices in Stripe (with correct recurring configuration).
- Update mapping env vars (
STRIPE_PLAN_PRICE_MAPPING_TEST/STRIPE_PLAN_PRICE_MAPPING_LIVE) with new price IDs. - Ensure a
SubscriptionPlanrow exists in the DB for the tier being mapped. - Redeploy backend. Startup validation will fail if required keys are missing or mapping JSON is malformed.
4. Checkout and Subscription Lifecycle
4.1 Create Checkout Session
Endpoint:
POST /api/subscription/create-checkout-session- File:
backend/api/subscription/routes/payment.py
- File:
Request body:
tier: SubscriptionTier(e.g."basic","pro")billing_cycle: BillingCycle(e.g."monthly")success_url: strcancel_url: str
Flow:
- Auth middleware resolves
current_useranduser_id. StripeService.create_checkout_session:- Fetches
price_idvia_get_price_id_for_plan. - Finds or creates Stripe Customer (with
user_idin metadata). - Creates a Stripe Checkout Session:
- Mode:
subscription. - Metadata: includes
user_idandprice_id.
- Mode:
- Fetches
- Returns
checkout_session.urlto the frontend.
Special handling:
- Metered prices:
- For metered prices,
quantityis omitted to comply with Stripe rules. - For non-metered prices,
quantityis set to1.
- For metered prices,
4.2 Customer Portal Session
Endpoint:
POST /api/subscription/create-portal-session
Flow:
- Lookup
UserSubscriptionandstripe_customer_id. - If missing, search Stripe by
metadata['user_id']. - Create Stripe Billing Portal session and return URL.
4.3 Webhook Handling
Endpoint:
POST /api/subscription/webhook- File:
backend/api/subscription/routes/payment.py - Delegates to
StripeService.handle_webhook.
- File:
Verification:
stripe.Webhook.construct_event(payload, sig_header, STRIPE_WEBHOOK_SECRET)is used to validate signatures.
Handled events:
checkout.session.completed- Retrieves subscription and price.
- Updates
UserSubscriptionto active and storesstripe_customer_idandstripe_subscription_id.
invoice.payment_succeeded- Sets
UserSubscription.statustoACTIVE. - Updates
current_period_endfrom invoice period.
- Sets
invoice.payment_failed- Sets status to
PAST_DUE,is_activefalse.
- Sets status to
customer.subscription.updated- Syncs status and
auto_renew.
- Syncs status and
customer.subscription.deleted- Marks subscription as cancelled and disables auto renew.
Helper:
_update_user_subscriptioncentralizes updating/creatingUserSubscriptionrecords based on Stripe data.
5. Disputes Integration
Backend routes:
- File:
backend/api/subscription/routes/disputes.py
Endpoints:
GET /api/subscription/disputes- Proxies
stripe.Dispute.list.
- Proxies
GET /api/subscription/disputes/{dispute_id}- Proxies
stripe.Dispute.retrieve.
- Proxies
POST /api/subscription/disputes/{dispute_id}- Proxies
stripe.Dispute.modifywithevidence.
- Proxies
POST /api/subscription/disputes/{dispute_id}/close- Proxies
stripe.Dispute.close.
- Proxies
Admin guard:
_ensure_admin(current_user)ensures:- Admin by email, domain, or role
"admin". - Can be bypassed only when
DISABLE_AUTH=true(local use).
- Admin by email, domain, or role
Frontend UI:
- File:
frontend/src/pages/StripeDisputesDashboard.tsx - Route:
/stripe-disputes - Disputes tab:
- Lists disputes and allows:
- Viewing details.
- Submitting evidence fields:
customer_email_address,customer_name,customer_purchase_ip,access_activity_log,uncategorized_text.
- Tagging a high-level fraud type, which is encoded into
uncategorized_text. - Closing the dispute.
- Lists disputes and allows:
6. Early Fraud Warnings (EFW) and Proactive Refunds
6.1 Ingestion
Model:
FraudWarninginbackend/models/subscription_models.py- Columns:
id,charge_id,payment_intent_id,user_id,amount,currency,status,action,action_at,reason_notes,metadata,created_at.
- Columns:
Ingestion logic:
StripeService._handle_early_fraud_warning:- Triggered for event types starting with
radar.early_fraud_warning.. - Retrieves the associated
Chargeto populate amount, currency, and metadata. - Infers
user_idfromcharge.metadata.user_idwhen available. - Upserts a
FraudWarningrow with status"open"and action"none". - Stores raw EFW and Charge data in
metadata.
- Triggered for event types starting with
6.2 Fraud Warnings API
File: backend/api/subscription/routes/fraud_warnings.py
Endpoints:
GET /api/subscription/fraud-warnings- Query params:
status(default"open")limit,offset
- Returns a list of warnings with core fields.
- Query params:
GET /api/subscription/fraud-warnings/{id}- Returns full details including
metadata.
- Returns full details including
POST /api/subscription/fraud-warnings/{id}/refund- Performs a full refund via
stripe.Refund.create(charge=...). - Updates
status="refunded",action="refund_full",action_atandreason_notes.
- Performs a full refund via
POST /api/subscription/fraud-warnings/{id}/ignore- Sets
status="ignored",action="ignored", updates notes.
- Sets
All endpoints apply the same admin guard used for disputes.
6.3 Frontend Fraud Warnings Tab
- File:
frontend/src/pages/StripeDisputesDashboard.tsx
Behavior:
- Adds a tabbed view:
- Tab 1: Disputes.
- Tab 2: Fraud Warnings.
- Fraud Warnings tab:
- Lists EFWs (from
/fraud-warnings). - Shows details including:
- Stripe EFW
fraud_type,actionableflag. - Amount, created time, internal status/action.
- Stripe EFW
- Allows:
- Proactive full refund (calls
/fraud-warnings/{id}/refund). - Mark as ignored (calls
/fraud-warnings/{id}/ignore). - Add/update internal notes.
- Proactive full refund (calls
- Lists EFWs (from
7. Rate Limiting for Checkout
Endpoint: POST /api/subscription/create-checkout-session
File: backend/api/subscription/routes/payment.py
Logic:
- Per-user in-memory rate limiting:
- Window: 60 seconds.
- Max requests: 10 within the window.
- On exceed:
- Logs a warning with
user_id, IP, attempts count. - Returns HTTP 429 with a friendly error message.
- Logs a warning with
Purpose:
- Protects against card testing and abuse by limiting how often a user can create Checkout sessions.
Considerations:
- For multi-instance deployments, a shared store (e.g. Redis) is recommended to make rate limiting consistent across instances.
8. Extending and Maintaining the Integration
Adding new subscription tiers or prices
- Create or update prices in Stripe.
- Update the relevant mapping environment variable (
STRIPE_PLAN_PRICE_MAPPING_TESTorSTRIPE_PLAN_PRICE_MAPPING_LIVE). - Ensure corresponding rows in
SubscriptionPlan. - Add any needed frontend logic (e.g. additional tiers in pricing UI).
Supporting additional Stripe events
- Extend
StripeService.handle_webhookwith new event types. - Implement corresponding handlers (
_handle_*) that:- Parse event data.
- Update your DB models.
- Log with enough context.
Making the system more robust
- Reintroduce idempotency keys for write operations (Checkout creation, refunds) using stable dedupe keys.
- Replace in-memory rate limiting with shared store-based limiting when scaling horizontally.
- Add more detailed logs/metrics around:
- New subscriptions.
- Failed payments.
- Disputes and early fraud warnings.