Subscriptions¶
app/config/subscriptions.yaml is the app-level catalog for paid access, capability gates, token wallets, token allowances, top-ups, add-ons, and pricing-page groups.
Add this file when the app is a SaaS app or when module actions need entitlement gates. Apps without paid access can omit it.
subscriptions.yaml does not provision infrastructure by itself. If a paid resource is managed by the host, the plan or add-on only grants the right to request it; the actual provisioning decision belongs to the host module or a module-owned commercial/service boundary.
How It Works¶
- Plans grant capability ids.
- Module actions declare an
entitlement_gate. - At startup, Mozaiks loads
app/config/subscriptions.yaml. - The runtime checks the current assignment store before dispatching gated module actions. A call whose capability no active plan grants returns HTTP 402 with
detail.error_code: ENTITLEMENT_REQUIRED.
Among HTTP calls, gates are skipped only for requests with development access. With AUTH_ENABLED=false, the value .env.example and mozaiks init ship, HTTP calls to /api/modules/... from this machine run as trusted local development and skip permission and entitlement checks, so a gated action succeeds for you. Callers without development access, such as anonymous visitors of a public app (AUTH_ANON_ACCESS=public), are checked. Visitors share one identity, so a plan assigned to it applies to every visitor. Turn authentication on to test gating per user.
Payment providers, invoices, taxes, payouts, and settlement stay behind app or managed-capability integrations. The subscriptions file defines the app's provider-neutral access contract.
For generated apps, SubscriptionContractDesigner selects plan features from the injected approved pricing feature inventory. Module feature IDs have the form module.<module_id>.<action_id> and identify approved owned_mutations, code-constructed canonical create_<entity> / update_<entity> / delete_<entity> writes, or declared custom_reads. Workflow features are excluded from the selectable inventory until workflow launch enforces plan grants (#770). The model selects module IDs in subscription_config_file.plans[].included_features; it does not invent capability IDs or action gate mappings. The factory derives stable feature.<feature_id> capability IDs (normalizing each ID segment to lowercase snake case when needed), runtime plans[].capabilities, and module_contract_updates for features that differ between plans, then applies those gates to module.yaml during task admission, assembly, and repair. An action included in every plan has no entitlement gate, so cancellation does not remove access to that shared action. The persisted contract also records selected_features_by_plan for downstream review. Generated add-ons may select required_feature from the same inventory, which code translates to the runtime required_capability field.
For example, when the approved tasks module has create_task, update_task, delete_task, and a custom summarize_tasks read, a limited Free plan selects the three core write features and sets a usage limit with feature_id: module.tasks.create_task. Pro selects those features plus module.tasks.summarize_tasks. Canonical list_tasks and get_tasks reads are available to both plans without a gate. The shared writes also have no gate; only summarize_tasks does. Usage limits are display-only: the runtime does not enforce a task count or other quota yet (#770). A paid view needs a declared custom read; canonical collection list/get actions and managed-pack facade actions are excluded from the feature inventory. Plan browsing, checkout, upgrades, the billing portal, usage, and token access remain available to free users.
Selecting an unavailable feature produces feedback listing the valid inventory and the option to remove it from the plan. If the product needs a missing custom read or write, DesignDocs must approve it before subscription design can use it. Generated YAML cannot override or remove the derived gate mapping. Writing a gated manifest, including replacing an existing gated manifest with an ungated repair, requires the approved subscription contract. Missing approval fails closed; an explicit approved no-subscription decision can remove old gates.
Multi-Product Catalog¶
Manually authored apps can use the v2 product catalog shape when they have more than one paid line, such as platform access, AI usage, hosting, domains, or marketing placement. SubscriptionContractDesigner emits v1 with optional pricing_catalog display groups.
schema_version: mozaiks.subscriptions.v2
label: Mozaiks Platform
default_product_id: platform
products:
- product_id: platform
label: Platform
default_plan_id: starter
assignment_store:
data_alias: billing.platform_subscriptions
user_id_field: user_id
plans:
- plan_id: starter
label: Starter
capabilities:
- apps.create
- apps.publish
- plan_id: builder
label: Builder
capabilities:
- apps.create
- apps.publish
- ai.chat
- product_id: ai
label: AI
default_plan_id: included
assignment_store:
data_alias: billing.ai_subscriptions
user_id_field: user_id
token_wallets:
- wallet_id: ai_tokens
label: AI token balance
unit: tokens
usage_meter_id: ai_tokens
scope: user
auto_debit_usage: true
plans:
- plan_id: included
label: Included
capabilities:
- ai.chat
token_allowances:
- wallet_id: ai_tokens
amount: 100000
cadence: monthly
- plan_id: ai_plus
label: AI Plus
capabilities:
- ai.chat
- ai.workflow.priority
token_allowances:
- wallet_id: ai_tokens
amount: 1000000
cadence: monthly
add_on_products:
- add_on_id: hero_weekly
label: Hero Placement - Weekly
kind: marketplace_placement
billing_mode: one_time
required_capability: marketplace.placements.order
capability_groups: [marketplace.placements]
duration_days: 7
price:
amount_cents: 9900
currency: usd
display: "$99/week"
pricing_catalog:
default_group_id: platform
groups:
- group_id: platform
label: Platform
kind: subscription
plan_ids: [starter, builder]
- group_id: ai
label: AI
kind: subscription
plan_ids: [included, ai_plus]
- group_id: marketing
label: Marketing
kind: add_on
add_on_ids: [hero_weekly]
assignment_store decides who holds a plan:
data_aliasis a data-contract alias, not a collection name. Declare it inapp/data/contract.json, for example"aliases": [{"alias": "billing.platform_subscriptions", "collection": "billing_platform_subscriptions"}], and write assignment records to that collection. If the alias is not declared, the adapter fails closed and denies every gated action, including capabilities the default plan grants.user_id_fieldis unset by default, which makes an assignment app- or tenant-wide: one record entitles every user in that scope. Setuser_id_field: user_idfor per-user plans; generated apps always do.
What Goes Here¶
| Concern | Field |
|---|---|
| Product line | products[].product_id |
| Plans | products[].plans[] |
| Capability gates | products[].plans[].capabilities[] |
| Current assignment store | products[].assignment_store |
| Usage limits (display only; not enforced yet) | products[].plans[].usage_limits[] |
| Included token grants | products[].plans[].token_allowances[] |
| Token wallet metadata | products[].token_wallets[] |
| Top-up products | products[].top_up_products[] |
| Non-token add-ons | add_on_products[] |
| Pricing-page grouping | pricing_catalog.groups[] or products[].pricing_catalog_group |
Token wallets may be declared at the root for a shared balance or inside one product. Repeated declarations of the same wallet ID must agree on the wallet definition. The token summary reports each wallet's governing product_id, plan_id, plan_resolution, allowance_source, and grant_authority; its top-level plan_id remains the primary product plan for older clients. A wallet referenced by more than one product has no single governing plan, so its plan allowances are marked ambiguous and automatic grants are skipped. An active assignment without a stored allowance snapshot is marked missing_snapshot; the summary does not substitute a catalog amount. Explicit grants into a shared wallet must identify their product. The ledger keeps the existing idempotency key for single-owner wallets and includes the product in the key for shared wallets. In v2, each usage meter may have only one auto-debit wallet. chat.usage_delta and its token preflight use the ai_tokens meter; wallets for other meters are not charged by chat usage.
The runtime may materialize default-plan token allowances. For an active v2 subscription assignment, billing fulfillment owns token credits from the stored allowance snapshot, including renewals. POST /api/me/tokens/sync and LLM usage preflight show the paid plan but do not mint another paid credit. The wallet balance and ledger entries show what has actually been granted. V1 single-product allowance behavior is unchanged.
Token wallet depleted_balance routes must be app-local paths beginning with a single /. Protocol-relative paths such as //provider.example/billing are invalid because browsers treat them as external destinations.
Module Gate Example¶
actions:
- id: summarize_tasks
description: Summarize the caller's tasks.
handler_method: summarize_tasks
entitlement_gate: feature.module.tasks.summarize_tasks
The factory derives this gate from the selected module.tasks.summarize_tasks feature. Any active plan that grants it allows the action to run.
Every action entry in module.yaml requires id, description, and handler_method; entitlement_gate is optional. The module loader rejects unknown action fields, so an entry keyed action_id fails validation. See Minimum module.yaml for the full action shape.
Custom Money Rules¶
Subscription access and provider-neutral add-on product definitions belong in app/config/subscriptions.yaml.
Module-owned commercial behavior belongs with the owning module. Examples: marketplace placement rules, usage fees, campaign terms, payout policy, or revenue-share display metadata can live in app/modules/{module_id}/contracts/commercial.yaml when that module owns the behavior.
Use a module service.yaml or commercial.yaml when a capability needs managed provisioning, BYOK fallback, or provider-specific fulfillment rules. Keep the app-level subscription file focused on entitlement and catalog presentation.
Use add_on_products[] for the app-level purchasable add-on catalog shown by pricing and billing surfaces. Do not put provider product ids, provider price ids, checkout sessions, order state, fulfillment state, inventory policy, invoices, taxes, or settlement records in subscriptions.yaml.