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
| Free | Pro | Team | Business | |
|---|---|---|---|---|
| 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 |
| Documents | 25 | 500 | 2,500 | 10,000 |
| Storage | 50 MB | 2 GB | 10 GB | 50 GB |
| Largest file | 10 MB | 25 MB | 50 MB | 50 MB |
| Models | Fast | Fast, Standard | All | All |
| Members (shown, not enforced) | 1 | 3 | 10 | 50 |
| 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:
| Tier | Models |
|---|---|
| Fast | GPT-5 Nano, GPT-5 Mini, GPT-4.1 Mini, Claude Haiku 4.5, Gemini 2.5 Flash and Flash Lite |
| Standard | GPT-5, GPT-4.1, o4-mini, Claude Sonnet 4.5 and 4.6, Gemini 2.5 Pro |
| Premium | Claude 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
| Limit | Where | What the user gets |
|---|---|---|
| AI credit | subscriptions.utils.has_sufficient_buffer, before every chat message, translation and transcription | 403, "Insufficient credits…" |
| Models | The same check, given the chat's model; also when a chat is created or switched to another model | 403 (400 on switching), "The Free plan doesn't include …" |
| Documents, storage, file size, credit | documents.limits.check_document_allowance: uploads, web pages and links added from chat | 403, 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 ProA 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
-
Edit
plans.py: prices, limits, models, the bullets shown on the pricing page. -
Preview the Stripe changes, then apply them:
python manage.py sync_stripe_plans --dry-run python manage.py sync_stripe_plansA 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.
-
The command then pulls products and prices into the database. Use
--skip-dbwhen the database isn't reachable from where you run it; the product and price webhooks, orpython 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.