Add step-by-step setup guide covering: - Getting API keys from Stripe Dashboard - Creating webhook endpoint and getting signing secret - Local development with Stripe CLI - Creating products and prices 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
Subscription & Billing System
The platform provides a comprehensive subscription and billing system for managing vendor subscriptions, usage limits, and payments through Stripe.
Overview
The billing system enables:
- Subscription Tiers: Database-driven tier definitions with configurable limits
- Usage Tracking: Orders, products, and team member limits per tier
- Stripe Integration: Checkout sessions, customer portal, and webhook handling
- Self-Service Billing: Vendor-facing billing page for subscription management
- Add-ons: Optional purchasable items (domains, SSL, email packages)
- Capacity Forecasting: Growth trends and scaling recommendations
- Background Jobs: Automated subscription lifecycle management
Architecture
Database Models
All subscription models are defined in models/database/subscription.py:
| Model | Purpose |
|---|---|
SubscriptionTier |
Tier definitions with limits and Stripe price IDs |
VendorSubscription |
Per-vendor subscription status and usage |
AddOnProduct |
Purchasable add-ons (domains, SSL, email) |
VendorAddOn |
Add-ons purchased by each vendor |
StripeWebhookEvent |
Idempotency tracking for webhooks |
BillingHistory |
Invoice and payment history |
CapacitySnapshot |
Daily platform capacity metrics for forecasting |
Services
| Service | Location | Purpose |
|---|---|---|
BillingService |
app/services/billing_service.py |
Subscription operations, checkout, portal |
SubscriptionService |
app/services/subscription_service.py |
Limit checks, usage tracking, tier info |
StripeService |
app/services/stripe_service.py |
Core Stripe API operations |
CapacityForecastService |
app/services/capacity_forecast_service.py |
Growth trends, projections |
PlatformHealthService |
app/services/platform_health_service.py |
Subscription capacity aggregation |
Background Tasks
| Task | Location | Schedule | Purpose |
|---|---|---|---|
reset_period_counters |
app/tasks/subscription_tasks.py |
Daily | Reset order counters at period end |
check_trial_expirations |
app/tasks/subscription_tasks.py |
Daily | Expire trials without payment method |
sync_stripe_status |
app/tasks/subscription_tasks.py |
Hourly | Sync status with Stripe |
cleanup_stale_subscriptions |
app/tasks/subscription_tasks.py |
Weekly | Clean up old cancelled subscriptions |
capture_capacity_snapshot |
app/tasks/subscription_tasks.py |
Daily | Capture capacity metrics snapshot |
API Endpoints
Vendor Billing API
All billing endpoints are under /api/v1/vendor/billing:
| Endpoint | Method | Purpose |
|---|---|---|
/billing/subscription |
GET | Current subscription status & usage |
/billing/tiers |
GET | Available tiers for upgrade |
/billing/checkout |
POST | Create Stripe checkout session |
/billing/portal |
POST | Create Stripe customer portal session |
/billing/invoices |
GET | Invoice history |
/billing/upcoming-invoice |
GET | Preview next invoice |
/billing/change-tier |
POST | Upgrade/downgrade tier |
/billing/addons |
GET | Available add-on products |
/billing/my-addons |
GET | Vendor's purchased add-ons |
/billing/addons/purchase |
POST | Purchase an add-on |
/billing/addons/{id} |
DELETE | Cancel an add-on |
/billing/cancel |
POST | Cancel subscription |
/billing/reactivate |
POST | Reactivate cancelled subscription |
Admin Platform Health API
Capacity endpoints under /api/v1/admin/platform-health:
| Endpoint | Method | Purpose |
|---|---|---|
/platform-health/health |
GET | Full platform health report |
/platform-health/capacity |
GET | Capacity-focused metrics |
/platform-health/subscription-capacity |
GET | Subscription-based capacity vs usage |
/platform-health/trends |
GET | Growth trends over time |
/platform-health/recommendations |
GET | Scaling recommendations |
/platform-health/snapshot |
POST | Manually capture capacity snapshot |
Subscription Tiers
Default Tiers
| Tier | Price | Products | Orders/mo | Team |
|---|---|---|---|---|
| Essential | €49/mo | 200 | 100 | 1 |
| Professional | €99/mo | Unlimited | 500 | 3 |
| Business | €199/mo | Unlimited | 2000 | 10 |
| Enterprise | Custom | Unlimited | Unlimited | Unlimited |
Database-Driven Tiers
Tiers are stored in the subscription_tiers table and queried via SubscriptionService:
# Get tier from database with legacy fallback
tier_info = subscription_service.get_tier_info("professional", db=db)
# Query all active tiers
all_tiers = subscription_service.get_all_tiers(db=db)
The service maintains backward compatibility with a legacy fallback:
def get_tier_info(self, tier_code: str, db: Session | None = None) -> TierInfo:
"""Get full tier information. Queries database if db session provided."""
if db is not None:
db_tier = self.get_tier_by_code(db, tier_code)
if db_tier:
return TierInfo(...)
# Fallback to hardcoded TIER_LIMITS during migration
return self._get_tier_from_legacy(tier_code)
Tier Features
Each tier includes specific features stored in the features JSON column:
tier.features = [
"basic_support", # Essential
"priority_support", # Professional+
"analytics", # Business+
"api_access", # Business+
"white_label", # Enterprise
"custom_integrations", # Enterprise
]
Limit Enforcement
Limits are enforced at the service layer:
Orders
# app/services/order_service.py
subscription_service.check_order_limit(db, vendor_id)
Products
# app/api/v1/vendor/products.py
subscription_service.check_product_limit(db, vendor_id)
Team Members
# app/services/vendor_team_service.py
subscription_service.can_add_team_member(db, vendor_id)
Stripe Integration
Configuration
Required environment variables:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_TRIAL_DAYS=30 # Optional, default trial period
Setup Guide
Step 1: Get API Keys
- Go to Stripe Dashboard
- Copy your Publishable key (
pk_test_...orpk_live_...) - Copy your Secret key (
sk_test_...orsk_live_...)
Step 2: Create Webhook Endpoint
- Go to Stripe Webhooks
- Click Add endpoint
- Enter your endpoint URL:
https://yourdomain.com/api/v1/webhooks/stripe - Select events to listen to:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failed
- Click Add endpoint
- Copy the Signing secret (
whsec_...) - this is yourSTRIPE_WEBHOOK_SECRET
Step 3: Local Development with Stripe CLI
For local testing, use the Stripe CLI:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe # macOS
# or download from https://github.com/stripe/stripe-cli/releases
# Login to Stripe
stripe login
# Forward webhooks to your local server
stripe listen --forward-to localhost:8000/api/v1/webhooks/stripe
# The CLI will display a webhook signing secret (whsec_...)
# Use this as STRIPE_WEBHOOK_SECRET for local development
Step 4: Create Products & Prices in Stripe
Create subscription products for each tier:
- Go to Stripe Products
- Create products for each tier (Starter, Professional, Business, Enterprise)
- Add monthly and annual prices for each
- Copy the Price IDs (
price_...) and update your tier configuration
Webhook Events
The system handles these Stripe events:
| Event | Handler |
|---|---|
checkout.session.completed |
Activates subscription, links customer |
customer.subscription.updated |
Updates tier, status, period |
customer.subscription.deleted |
Marks subscription cancelled |
invoice.paid |
Records payment, resets counters |
invoice.payment_failed |
Marks past due, increments retry count |
Webhook Endpoint
Webhooks are received at /api/v1/webhooks/stripe:
# Uses signature verification for security
event = stripe_service.construct_event(payload, stripe_signature)
Vendor Billing Page
The vendor billing page is at /vendor/{vendor_code}/billing:
Page Sections
- Current Plan: Tier name, status, next billing date
- Usage Meters: Products, orders, team members with limits
- Change Plan: Upgrade/downgrade options
- Payment Method: Link to Stripe portal
- Invoice History: Recent invoices with PDF links
- Add-ons: Available and purchased add-ons
JavaScript Component
The billing page uses Alpine.js (static/vendor/js/billing.js):
function billingData() {
return {
subscription: null,
tiers: [],
invoices: [],
addons: [],
myAddons: [],
async init() {
await this.loadData();
},
async selectTier(tier) {
const response = await this.apiPost('/billing/checkout', {
tier_code: tier.code,
is_annual: false
});
window.location.href = response.checkout_url;
},
async openPortal() {
const response = await this.apiPost('/billing/portal', {});
window.location.href = response.portal_url;
},
async purchaseAddon(addon) {
const response = await this.apiPost('/billing/addons/purchase', {
addon_code: addon.code
});
window.location.href = response.checkout_url;
},
async cancelAddon(addon) {
await this.apiDelete(`/billing/addons/${addon.id}`);
await this.loadMyAddons();
}
};
}
Add-ons
Available Add-ons
| Code | Name | Category | Price |
|---|---|---|---|
domain |
Custom Domain | domain | €15/year |
ssl_premium |
Premium SSL | ssl | €49/year |
email_5 |
5 Email Addresses | €5/month | |
email_10 |
10 Email Addresses | €9/month | |
email_25 |
25 Email Addresses | €19/month |
Purchase Flow
- Vendor selects add-on on billing page
- For domains: enter domain name, validate availability
- Create Stripe checkout session with add-on price
- On webhook success: create
VendorAddOnrecord
Background Tasks
Task Definitions
# app/tasks/subscription_tasks.py
async def reset_period_counters():
"""
Reset order counters for subscriptions whose billing period has ended.
Should run daily. Resets orders_this_period to 0 and updates period dates.
"""
async def check_trial_expirations():
"""
Check for expired trials and update their status.
Trials without payment method -> expired
Trials with payment method -> active
"""
async def sync_stripe_status():
"""
Sync subscription status with Stripe.
Fetches current status and updates local records.
"""
async def cleanup_stale_subscriptions():
"""
Clean up subscriptions in inconsistent states.
Marks old cancelled subscriptions as expired.
"""
async def capture_capacity_snapshot():
"""
Capture a daily snapshot of platform capacity metrics.
Used for growth trending and capacity forecasting.
"""
Scheduling
Configure with your scheduler of choice (Celery, APScheduler, cron):
# Example with APScheduler
scheduler.add_job(reset_period_counters, 'cron', hour=0, minute=5)
scheduler.add_job(check_trial_expirations, 'cron', hour=1, minute=0)
scheduler.add_job(sync_stripe_status, 'cron', minute=0) # Every hour
scheduler.add_job(cleanup_stale_subscriptions, 'cron', day_of_week=0) # Weekly
scheduler.add_job(capture_capacity_snapshot, 'cron', hour=0, minute=0)
Capacity Forecasting
Subscription-Based Capacity
Track theoretical vs actual capacity:
capacity = platform_health_service.get_subscription_capacity(db)
# Returns:
{
"total_subscriptions": 150,
"tier_distribution": {
"essential": 80,
"professional": 50,
"business": 18,
"enterprise": 2
},
"products": {
"actual": 125000,
"theoretical_limit": 500000,
"utilization_percent": 25.0,
"headroom": 375000
},
"orders_monthly": {
"actual": 45000,
"theoretical_limit": 300000,
"utilization_percent": 15.0
},
"team_members": {
"actual": 320,
"theoretical_limit": 1500,
"utilization_percent": 21.3
}
}
Growth Trends
Analyze growth over time:
trends = capacity_forecast_service.get_growth_trends(db, days=30)
# Returns:
{
"period_days": 30,
"snapshots_available": 30,
"trends": {
"vendors": {
"start_value": 140,
"current_value": 150,
"change": 10,
"growth_rate_percent": 7.14,
"daily_growth_rate": 0.238,
"monthly_projection": 161
},
"products": {
"start_value": 115000,
"current_value": 125000,
"change": 10000,
"growth_rate_percent": 8.7,
"monthly_projection": 136000
}
}
}
Scaling Recommendations
Get automated scaling advice:
recommendations = capacity_forecast_service.get_scaling_recommendations(db)
# Returns:
[
{
"category": "capacity",
"severity": "warning",
"title": "Product capacity approaching limit",
"description": "Currently at 85% of theoretical product capacity",
"action": "Consider upgrading vendor tiers or adding capacity"
},
{
"category": "infrastructure",
"severity": "info",
"title": "Current tier: Medium",
"description": "Next upgrade trigger: 300 vendors",
"action": "Monitor growth and plan for infrastructure scaling"
}
]
Infrastructure Scaling Reference
| Clients | vCPU | RAM | Storage | Database | Monthly Cost |
|---|---|---|---|---|---|
| 1-50 | 2 | 4GB | 100GB | SQLite | €30 |
| 50-100 | 4 | 8GB | 250GB | PostgreSQL | €80 |
| 100-300 | 4 | 16GB | 500GB | PostgreSQL | €150 |
| 300-500 | 8 | 32GB | 1TB | PostgreSQL + Redis | €350 |
| 500-1000 | 16 | 64GB | 2TB | PostgreSQL + Redis | €700 |
| 1000+ | 32+ | 128GB+ | 4TB+ | PostgreSQL cluster | €1,500+ |
Exception Handling
Custom exceptions for billing operations (app/exceptions/billing.py):
| Exception | HTTP Status | Description |
|---|---|---|
PaymentSystemNotConfiguredException |
503 | Stripe not configured |
TierNotFoundException |
404 | Invalid tier code |
StripePriceNotConfiguredException |
400 | No Stripe price for tier |
NoActiveSubscriptionException |
400 | Operation requires subscription |
SubscriptionNotCancelledException |
400 | Cannot reactivate active subscription |
Testing
Unit tests for the billing system:
# Run billing service tests
pytest tests/unit/services/test_billing_service.py -v
# Run webhook handler tests
pytest tests/unit/services/test_stripe_webhook_handler.py -v
Test Coverage
BillingService: Subscription queries, checkout, portal, cancellationStripeWebhookHandler: Event idempotency, checkout completion, invoice handling
Migration
Creating Tiers
Tiers are seeded via migration:
# alembic/versions/xxx_add_subscription_billing_tables.py
def seed_subscription_tiers(op):
op.bulk_insert(subscription_tiers_table, [
{
"code": "essential",
"name": "Essential",
"price_monthly_cents": 4900,
"orders_per_month": 100,
"products_limit": 200,
"team_members": 1,
},
# ... more tiers
])
Capacity Snapshots Table
The capacity_snapshots table stores daily metrics:
# alembic/versions/l0a1b2c3d4e5_add_capacity_snapshots_table.py
class CapacitySnapshot(Base):
id: int
snapshot_date: datetime # Unique per day
# Vendor metrics
total_vendors: int
active_vendors: int
trial_vendors: int
# Subscription metrics
total_subscriptions: int
active_subscriptions: int
# Resource metrics
total_products: int
total_orders_month: int
total_team_members: int
# Storage metrics
storage_used_gb: Decimal
db_size_mb: Decimal
# Capacity metrics
theoretical_products_limit: int
theoretical_orders_limit: int
theoretical_team_limit: int
# Tier distribution
tier_distribution: dict # JSON
Setting Up Stripe
- Create products and prices in Stripe Dashboard
- Update
SubscriptionTierrecords with Stripe IDs:
tier.stripe_product_id = "prod_xxx"
tier.stripe_price_monthly_id = "price_xxx"
tier.stripe_price_annual_id = "price_yyy"
- Configure webhook endpoint in Stripe Dashboard:
- URL:
https://yourdomain.com/api/v1/webhooks/stripe - Events:
checkout.session.completed,customer.subscription.*,invoice.*
- URL:
Security Considerations
- Webhook signatures verified before processing
- Idempotency keys prevent duplicate event processing
- Customer portal links are session-based and expire
- Stripe API key stored securely in environment variables
- Background tasks run with database session isolation
Related Documentation
- Capacity Monitoring - Detailed monitoring guide
- Capacity Planning - Infrastructure sizing
- Stripe Integration - Payment setup for vendors