HyperSaaS
BackendSubscriptions

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 ID

Links Django users to Stripe customers. Created automatically during checkout or customer sync.

Key properties:

  • current_subscription_items — Active/trialing/past_due subscription items
  • subscribed_products — Set of Product instances the user has access to
  • subscribed_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

StatusAccess GrantedDescription
activeYesPayment current
trialingYesIn free trial period
past_dueYesPayment failed, retrying
canceledNoSubscription cancelled
incompleteNoInitial payment failed
incomplete_expiredNoInitial payment window expired
unpaidNoAll retry attempts failed
endedNoSubscription terminated

API Endpoints

MethodEndpointAuthDescription
GET/api/subscriptions/my-subscription/YesList user's subscriptions
GET/api/subscriptions/my-subscription-items/YesList active subscription items
GET/api/subscriptions/subscribable-product/NoList available prices
POST/api/subscriptions/checkout/YesCreate checkout session
POST/api/subscriptions/webhook/NoStripe webhook receiver
POST/api/subscriptions/customer-portal/YesGet 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

SettingDescription
STRIPE_API_SECRETStripe secret API key
STRIPE_WEBHOOK_SECRETWebhook signature verification secret
FRONT_END_BASE_URLFrontend URL for checkout redirects
NEW_USER_FREE_TRIAL_DAYSDays of free trial (default: 7)
DEFAULT_PAYMENT_METHOD_TYPES["card"]
DEFAULT_CHECKOUT_MODE"subscription"
ALLOW_PROMOTION_CODEStrue

On this page