HyperSaaS
BackendSubscriptions

Plans

Free, Pro, Team and Business plans, defined in code and enforced across the app.

Every workspace is on one plan. The plans, their prices and what each allows are defined once, in backend/subscriptions/plans.py. Stripe holds the paid plans' products and prices, created from that file by one command. What a plan allows is always read from the code, never from Stripe.

The Plans

FreeProTeamBusiness
Price per workspace$0$20/mo · $200/yr$60/mo · $600/yr$200/mo · $2,000/yr
AI credit$1, once$8 / month$24 / month$80 / month
Documents255002,50010,000
Storage50 MB2 GB10 GB50 GB
Largest file10 MB25 MB50 MB50 MB
ModelsFastFast, StandardAllAll
Members (shown, not enforced)131050
Teams (shown, not enforced)——✓✓

A year costs ten months. Paid plans start with a 7-day trial (NEW_USER_FREE_TRIAL_DAYS), during which the AI credit is $1 (TRIAL_AI_CREDIT); every other limit is the plan's own from the start. AI credit is at provider cost; see AI Usage & Credit for how it's counted and renewed.

Model tiers, by what they cost to run:

TierModels
FastGPT-5 Nano, GPT-5 Mini, GPT-4.1 Mini, Claude Haiku 4.5, Gemini 2.5 Flash and Flash Lite
StandardGPT-5, GPT-4.1, o4-mini, Claude Sonnet 4.5 and 4.6, Gemini 2.5 Pro
PremiumClaude Opus 4.6, o3

A new chat that doesn't choose a model gets the plan's default: Gemini 2.5 Flash on Free, Gemini 2.5 Pro on paid plans.

Defining a Plan

PRO = Plan(
    key="pro",                      # also the Stripe product's metadata.plan
    name="Pro",
    tagline="For individuals and small teams.",
    monthly_price_cents=2000,       # a year costs ten months
    ai_credit=Decimal("8.00"),      # USD at provider cost, per month
    max_documents=500,
    max_storage_bytes=2 * GB,
    max_file_size_bytes=25 * MB,
    models=FAST_MODELS | STANDARD_MODELS,
    default_model=m.GEMINI_2_5_PRO,
    features=(                      # bullets on the pricing page
        "Up to 3 members",
        "Fast and standard AI models",
        "500 documents, 2 GB",
        "Files up to 25 MB",
        "Monthly AI usage included",
    ),
)

What's Enforced, and Where

LimitWhereWhat the user gets
AI creditsubscriptions.utils.has_sufficient_buffer, before every chat message, translation and transcription403, "Insufficient credits…"
ModelsThe same check, given the chat's model; also when a chat is created or switched to another model403 (400 on switching), "The Free plan doesn't include …"
Documents, storage, file size, creditdocuments.limits.check_document_allowance: uploads, web pages and links added from chat403, naming the limit

The storage limit counts the size declared when an upload starts. The presigned S3 URL only accepts a file of exactly that size, so the declared size is the real one.

Largest-file limits stay within DOCUMENT_MAX_UPLOAD_SIZE (50 MB), the size ingestion has been proven at. Raise both together.

How a Workspace Finds Its Plan

Each paid plan is one Stripe product with metadata.plan set to the plan's key (pro, team, business), and two prices found by lookup key: pro_month and pro_year. The product webhook and update_stripe_products copy metadata.plan into Product.plan.

from backend.subscriptions.plans import plan_for_workspace

plan = plan_for_workspace(workspace)
plan.includes_model("claude-opus-4-6")  # False on Free and Pro

A workspace is on its subscription's plan while the subscription grants access (active, trialing or past due), and on Free otherwise. A subscription to a product with no plan key counts as Free.

Changing a Plan

  1. Edit plans.py: prices, limits, models, the bullets shown on the pricing page.

  2. Preview the Stripe changes, then apply them:

    python manage.py sync_stripe_plans --dry-run
    python manage.py sync_stripe_plans

    A changed price becomes a new Stripe price that takes over the lookup key, and the old price is archived. Existing subscribers stay on what they pay until they change plan. Products that aren't a plan are archived. The billing portal's default configuration is set to offer exactly these plans and prices, which is where customers switch plan. Running it again changes nothing.

  3. The command then pulls products and prices into the database. Use --skip-db when the database isn't reachable from where you run it; the product and price webhooks, or python manage.py update_stripe_products, catch it up.

Limit changes (documents, credit, models) take effect for every workspace on that plan as soon as the code is deployed. They need no Stripe change unless the bullets or prices change too.

Going live: run sync_stripe_plans once with the live-mode key, after a --dry-run.

On this page