Overview
Stripe-based subscription billing with webhook sync.
The subscriptions module integrates Stripe for billing, providing checkout sessions, customer portal access, webhook-driven data sync, and feature-based access control.
Architecture
Frontend Backend Stripe
│ │ │
│ POST /checkout/ │ │
│─────────────────────────►│ stripe.checkout.create() │
│ │───────────────────────────►│
│ ◄── session_id ─────── │ │
│ │ │
│ Redirect to Stripe ────────────────────────────────►│
│ │ │
│ │ ◄── webhook events ───────│
│ │ (subscription.created) │
│ │ Update local models │
│ │ │
│ POST /customer-portal/ │ │
│─────────────────────────►│ portal.create() │
│ ◄── portal URL ────────│───────────────────────────►│Core Models
StripeUser
class StripeUser(models.Model):
user = models.OneToOneField(AUTH_USER_MODEL, primary_key=True)
customer_id = models.CharField(max_length=128, null=True) # Stripe customer IDLinks Django users to Stripe customers. Created automatically during checkout or customer sync.
Key properties:
current_subscription_items— Active/trialing/past_due subscription itemssubscribed_products— Set of Product instances the user has access tosubscribed_features— Set of Feature instances derived from subscribed products
Product & Price
class Product(models.Model):
product_id = models.CharField(primary_key=True) # Stripe Product ID
active = models.BooleanField()
name = models.CharField(max_length=256)
description = models.CharField(max_length=1024, null=True)
class Price(models.Model):
price_id = models.CharField(primary_key=True) # Stripe Price ID
product = models.ForeignKey(Product, related_name="prices")
nickname = models.CharField(max_length=256, null=True)
price = models.PositiveIntegerField() # Amount in cents
freq = models.CharField(max_length=64) # "month_1", "year_1"
active = models.BooleanField()
currency = models.CharField(max_length=3)Products and prices are synced from Stripe via webhooks or management commands.
Feature & ProductFeature
class Feature(models.Model):
feature_id = models.CharField(max_length=64, primary_key=True)
description = models.CharField(max_length=256, null=True)
class ProductFeature(models.Model):
product = models.ForeignKey(Product, related_name="linked_features")
feature = models.ForeignKey(Feature, related_name="linked_products")Features are defined in Stripe Product metadata as a space-delimited string (e.g., "chat rag export"). The sync process creates Feature and ProductFeature records automatically.
Subscription & SubscriptionItem
class Subscription(models.Model):
subscription_id = models.CharField(primary_key=True) # Stripe Subscription ID
stripe_user = models.ForeignKey(StripeUser, related_name="subscriptions")
period_start = models.DateTimeField(null=True)
period_end = models.DateTimeField(null=True)
cancel_at = models.DateTimeField(null=True)
cancel_at_period_end = models.BooleanField()
ended_at = models.DateTimeField(null=True)
status = models.CharField(max_length=64)
trial_start = models.DateTimeField(null=True)
trial_end = models.DateTimeField(null=True)
class SubscriptionItem(models.Model):
sub_item_id = models.CharField(primary_key=True)
subscription = models.ForeignKey(Subscription, related_name="items")
price = models.ForeignKey(Price)
quantity = models.PositiveIntegerField()Subscription Statuses
| Status | Access Granted | Description |
|---|---|---|
active | Yes | Payment current |
trialing | Yes | In free trial period |
past_due | Yes | Payment failed, retrying |
canceled | No | Subscription cancelled |
incomplete | No | Initial payment failed |
incomplete_expired | No | Initial payment window expired |
unpaid | No | All retry attempts failed |
ended | No | Subscription terminated |
API Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/subscriptions/my-subscription/ | Yes | List user's subscriptions |
| GET | /api/subscriptions/my-subscription-items/ | Yes | List active subscription items |
| GET | /api/subscriptions/subscribable-product/ | No | List available prices |
| POST | /api/subscriptions/checkout/ | Yes | Create checkout session |
| POST | /api/subscriptions/webhook/ | No | Stripe webhook receiver |
| POST | /api/subscriptions/customer-portal/ | Yes | Get customer portal URL |
Credit System Integration
The subscriptions module integrates with workspace cost tracking:
# subscriptions/utils.py
def has_sufficient_buffer(user, workspace, buffer=Decimal("0.01")):
credit_limit = get_user_credit_limit(user)
current_cost = get_workspace_cost(workspace)
if current_cost >= (credit_limit - buffer):
raise PermissionDenied("Credit limit exceeded")Credit limits are mapped from subscription prices via PLAN_CREDIT_MAPPING in settings. Users without subscriptions get DEFAULT_FREE_USAGE_CREDIT.
Configuration
| Setting | Description |
|---|---|
STRIPE_API_SECRET | Stripe secret API key |
STRIPE_WEBHOOK_SECRET | Webhook signature verification secret |
FRONT_END_BASE_URL | Frontend URL for checkout redirects |
NEW_USER_FREE_TRIAL_DAYS | Days of free trial (default: 7) |
DEFAULT_PAYMENT_METHOD_TYPES | ["card"] |
DEFAULT_CHECKOUT_MODE | "subscription" |
ALLOW_PROMOTION_CODES | true |