HyperSaaS
BackendSubscriptions

Models

Stripe data models mirrored in Django.

The subscriptions module mirrors Stripe's data model in Django, keeping a local copy of products, prices, and subscriptions for fast access.

Model Relationships

StripeUser (1:1 with Django User)

    ├── Subscription (1:N)
    │       │
    │       └── SubscriptionItem (1:N)
    │               │
    │               └── Price (FK)
    │                     │
    │                     └── Product (FK)
    │                           │
    │                           └── ProductFeature (M2M) → Feature

    └── Properties:
        ├── subscribed_products → Set[Product]
        └── subscribed_features → Set[Feature]

StripeUser

class StripeUser(models.Model):
    user = models.OneToOneField(AUTH_USER_MODEL, on_delete=models.CASCADE, primary_key=True)
    customer_id = models.CharField(max_length=128, null=True, blank=True)

The customer_id is assigned when a Stripe customer is created (during checkout or management command sync). Key computed properties:

@property
def current_subscription_items(self):
    """SubscriptionItems with access-granting status (active, trialing, past_due)."""
    return SubscriptionItem.objects.filter(
        subscription__stripe_user=self,
        subscription__status__in=ACCESS_GRANTING_STATUSES,
    )

@property
def subscribed_products(self):
    """Set of Products the user currently has access to."""
    return {item.price.product for item in self.current_subscription_items}

@property
def subscribed_features(self):
    """Set of Features derived from subscribed products."""
    features = set()
    for product in self.subscribed_products:
        features.update(
            Feature.objects.filter(linked_products__product=product)
        )
    return features

Product

class Product(models.Model):
    product_id = models.CharField(max_length=256, primary_key=True)
    active = models.BooleanField()
    description = models.CharField(max_length=1024, null=True, blank=True)
    name = models.CharField(max_length=256, null=True, blank=True)

Synced from Stripe via webhooks (product.created, product.updated, product.deleted).

Price

class Price(models.Model):
    price_id = models.CharField(max_length=256, primary_key=True)
    product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="prices")
    nickname = models.CharField(max_length=256, null=True, blank=True)
    price = models.PositiveIntegerField()  # Amount in cents (e.g., 2900 = $29.00)
    freq = models.CharField(max_length=64, null=True, blank=True)  # "month_1", "year_1"
    active = models.BooleanField()
    currency = models.CharField(max_length=3)  # "usd", "eur", etc.

The freq field is derived from Stripe's price.recurring.interval and interval_count:

  • month_1 = monthly billing
  • year_1 = annual billing
  • month_3 = quarterly billing

Feature

class Feature(models.Model):
    feature_id = models.CharField(max_length=64, primary_key=True)
    description = models.CharField(max_length=256, null=True, blank=True)

Features are defined in Stripe Product metadata. Add a features key with space-delimited feature IDs:

Product metadata in Stripe Dashboard:
  features: "chat rag export analytics"

The sync process creates Feature records and ProductFeature associations automatically.

Subscription

class Subscription(models.Model):
    subscription_id = models.CharField(max_length=256, primary_key=True)
    stripe_user = models.ForeignKey(StripeUser, on_delete=models.CASCADE, 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)

SubscriptionItem

class SubscriptionItem(models.Model):
    sub_item_id = models.CharField(max_length=256, primary_key=True)
    subscription = models.ForeignKey(Subscription, on_delete=models.CASCADE, related_name="items")
    price = models.ForeignKey(Price, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()

Query Helpers

The module provides query functions for common access patterns:

# List user's active subscriptions
list_user_subscriptions(user_id, current=True)

# List user's active subscription items (with product/price details)
list_user_subscription_items(user_id, current=True)

# Get products the user is currently subscribed to
list_user_subscription_products(user_id, current=True)

# Get prices the user can subscribe to (excludes current)
list_subscribable_product_prices_to_user(user_id)

# Get all active prices (public, for pricing page)
list_all_available_product_prices(expand=None)

When current=True, queries filter by ACCESS_GRANTING_STATUSES: active, past_due, trialing.

Pydantic Validation Models

Stripe API responses are validated with Pydantic models before being processed:

ModelPurpose
StripeEventWebhook event payload
StripeSubscriptionSubscription data with items
StripeProductProduct data with metadata
StripePricePrice data with recurring info
StripeCustomerCustomer data
StripeInvoiceInvoice with line items
StripeCurrency140+ currency code enum

These models ensure type safety when processing webhook payloads.

On this page