Plans and Quotas
What this is
Every ComStack project has a set of capabilities — which features are active, how many pages it can hold, how many locales it supports, whether it can make phone calls. This page describes the system that controls all of it: modules, plans, tiers, and templates.
How it works
The entitlement system has four layers:
| Term | What it means |
|---|---|
| Module | A feature capability — voice_web, voice_phone, whatsapp, knowledge_base, custom_pages, website, github_sync, chrome_ingest, api_keys. Each can be enabled or disabled independently. |
| Plan | The materialised entitlement document (settings/plan) that defines what a project is allowed to do right now. Contains per-module limits and project-wide constraints. |
| Tier | The commercial level — trial or standard. Determines which plan limits apply. |
| Template | A project template that defines the structural blueprint — which modules are available, what limits apply per tier, and trial duration. |
| Quota | A numeric limit within a plan — max_pages, max_locales, max_members, max_numbers, max_keys, max_sessions_per_day. |
Key principle: Enforcement code reads ONE document — settings/plan. It never needs to know about templates, coupons, or subscriptions. This makes the enforcement layer dead simple and the commercial layer infinitely flexible.
When to use it
Consult this page when you need to understand why a project operation is returning a quota error, what limits apply to a given tier, or how plan limits are materialised from project templates.
Parameters / fields / inputs
Modules
Module configuration lives on the project root document at modules.*.
| Module | Purpose | Key config fields |
|---|---|---|
voice_web | Browser-based live voice agent | enabled, voice, greeting, instructions |
voice_phone | PSTN phone calls via Twilio | enabled, numbers[] |
whatsapp | WhatsApp Business voice via Twilio | enabled, numbers[] |
knowledge_base | Agent-optimised documentation pages | enabled, access, locales[] |
custom_pages | Human-facing rich content pages | enabled, access, locales[] |
website | Published Starlight documentation site | enabled |
github_sync | Sync content from a GitHub repository | enabled |
chrome_ingest | Ingest web pages via Chrome Extension | enabled |
api_keys | MCP/HTTP API key authentication | enabled |
access is now only a default-seed. Per-page gating moved to the doc-level audience field (public · agents · members · internal). The module access value seeds the audience of new pages of that type and is otherwise inert.
Plan schema
{ "tier": "trial", "status": "active", "modules": { "website": { "enabled": true, "max_pages": 10 }, "voice_web": { "enabled": true, "max_sessions_per_day": 5 }, "voice_phone": { "enabled": false }, "whatsapp": { "enabled": false }, "knowledge_base": { "enabled": true, "max_pages": 20, "max_locales": 2 }, "custom_pages": { "enabled": true, "max_pages": 10, "max_locales": 2 }, "github_sync": { "enabled": false }, "chrome_ingest": { "enabled": true }, "api_keys": { "enabled": true, "max_keys": 1 } }, "max_members": 2, "expires_at": "2026-05-24T14:00:00Z", "source": "trial", "coupon_code": null}| Field | Type | Description |
|---|---|---|
tier | string | "trial" or "standard". Informational — enforcement uses the limits, not the tier name. |
status | string | "active" or "suspended". A suspended plan blocks all project operations. |
modules | map | Per-module entitlement. Each key matches a module name. |
max_members | number | Maximum project members across all roles. |
expires_at | Timestamp | null | When the plan expires. null = no expiry (paid plans). |
source | string | How this plan was created: "trial", "coupon", "migration", "admin", or "stripe". |
coupon_code | string | null | The coupon code redeemed to activate this plan. null for trials. |
Tiers
Trial
- Not all templates allow trials — check
trial.allowedon the template. - Duration is configurable per template (default: 14 days), stored in
expires_at. - Limits: restrictive — fewer pages, fewer locales, no phone/WhatsApp, limited members.
- When
expires_atpasses, all authenticated endpoints return403 Trial expired. Project data is preserved — upgrading restores access.
Standard
- Activated via coupon, admin override, or payment.
- No expiry (
expires_at: null). - Limits: generous — 200+ pages, 20 locales, phone/WhatsApp enabled, 50 members.
Enforcement
All enforcement reads settings/plan. Enforcement failures return 403.
| Check | Plan field |
|---|---|
| Module enabled? | modules.{type}.enabled |
| Page limit | modules.knowledge_base.max_pages or modules.custom_pages.max_pages |
| Locale limit | modules.knowledge_base.max_locales |
| Member limit | max_members |
| API key limit | modules.api_keys.max_keys |
| Phone number limit | modules.voice_phone.max_numbers |
| Live sessions/day | modules.voice_web.max_sessions_per_day |
| Trial expired? | expires_at |
The live-session quota is enforced atomically before a Gemini Live token is minted — a Firestore transaction reads the per-(project, user, UTC-day) counter and refuses with 429 if the limit is reached, preventing concurrent burst past the limit.
Template → Plan materialisation
When a project is created:
- Read the project template.
- Determine tier:
trialif no coupon andtrial.allowed;standardif coupon provided. - For each module in the template: if
available: false, skip; otherwise read the tier-specific config and write tosettings/plan. - Set
max_membersfrom the tier root. - Set
expires_atfromtrial.duration_days(ornullfor standard).
Upgrading patches settings/plan directly — no enforcement code changes needed.
Project templates
Two templates are available today:
| Template | Best for |
|---|---|
real-estate-eu | EU/Spain real estate agencies — voice, multilingual, property listings |
generic-business | Any business without a dedicated vertical — FAQ/knowledge-heavy, multi-locale, voice |
Onboarding picks the template based on the extracted industry (real-estate industries → real-estate-eu; everything else → generic-business). The selection is always shown to the user and is overridable.
Project status lifecycle
Separate from the plan, each project has a status field:
| Status | Site accessible? | Can publish? | Can configure? |
|---|---|---|---|
setup | Hosting exists but nothing deployed | Yes (preview) | Yes |
live | Public at site_url | Yes | Yes |
paused | Serves maintenance page | No | Yes |
archived | Site deleted | No | No |
Going live requires: at least one page, a site title set, and an explicit request to set status: "live".
Example
Checking why a page creation is failing with a quota error:
- Call
get-project-state— the response includes plan details and module status. - Check
modules.knowledge_base.max_pagesagainst the current live page count. - If at the limit, you need a plan upgrade (contact support) or delete archived pages first.
A 403 with "error": "Quota exceeded: knowledge_base.max_pages (20)" means the project is at its plan limit. A 403 with "error": "Trial expired" means expires_at has passed.
Common errors
| Error | Cause | Fix |
|---|---|---|
403 Quota exceeded: knowledge_base.max_pages | Hit the page limit for the current plan | Upgrade the plan or remove archived pages |
403 Trial expired | expires_at passed | Contact support to upgrade |
403 Module not enabled | The module is disabled in settings/plan | Enable the module via the module settings API |
429 on live session creation | max_sessions_per_day reached | Wait until UTC midnight reset or upgrade for a higher limit |
Related
- MCP Server overview — the MCP tools that surface plan and module state
- Permissions — role and capability matrix