Billing & Usage

Plans, Entitlements & Clearing Limits

A plan is not a separate system — it is an entitlement item; how tier gating, enforcement postures, and provider-driven lifecycle add to the entitlement model.

A "plan" in Ductor is not a separate subsystem. It is an entitlement item. The whole concept model — sources feeding snapshots feeding decisions, and the capability projection those decisions drive — belongs to Concepts → Entitlements and Auth → Entitlements. This page covers only what plans and billing add on top of that model.

A plan tier is an entitlement item

Plan tier is expressed inside the entitlement snapshot as a plan_tier item (ItemPlanTier). Billing and subscription state maps into entitlement sources of kind external_billing — a source grants a plan-tier item plus the feature and limit items that tier unlocks.

External billing external_billing source plan_tier item feature and limit items Entitlement snapshot Evaluation decision

Source and item shapes

Sources and items are defined in the entitlement domain. Each source carries a priority and a status; items carry a kind and an effect.

Source kindMeaning
external_billingSubscription/billing state (usually from the provider)
manual_contractA negotiated contract grant
operator_overrideAn operator-applied override
deployment_defaultDeployment-wide baseline
systemSystem-level grant
  • Source status: active | stale | revoked.
  • Item kinds: feature, limit, capability, plan_tier, contract.
  • Item effects: allow | deny. An explicit deny always wins.

Plan-tier gating

Plan-tier gating uses the requirement operator plan_tier_at_least (OpPlanTierAtLeast). It compares the snapshot's plan_tier item against a required tier using a fixed rank ladder — a request passes when the tenant's rank is greater than or equal to the required rank.

Tier valueRank
enterprise4
growth, growth-v2, pro3
starter, starter-v22
free1
anything else / unknown0

An unrecognized or missing tier ranks 0, so it satisfies no plan_tier_at_least requirement above the floor.

Enforcement postures

Enforcement is the billing-adjacent posture knob: it decides what happens when a tenant's required entitlement snapshot is missing, stale, or the evaluator is unavailable. These postures are covered in depth on Auth → Entitlements — summarized here.

ModeBehavior
passthroughZero value; honors the per-request fail-closed flag exactly as the caller set it
offLegacy fail-open for missing/stale/unavailable facts; explicit denies still deny
reportDefault — admit, but emit a loud grace-admit event for every request that would deny under strict
strictFail closed on missing/stale/unavailable facts

The mode is set via the entitlement.enforcement config key (DUCTOR_ENTITLEMENT_ENFORCEMENT), parsed by ParseEnforcementMode. In strict mode the entitlement.grace_unprovisioned flag (EntitlementConfig.EntitlementGraceUnprovisioned, default true) admits a tenant that has never had any source or snapshot under a bounded, observable bootstrap grace, so flipping strict on does not instantly deny un-onboarded tenants. The policy is wired at startup in cmd/ductor/fx_entitlement.go.

export DUCTOR_ENTITLEMENT_ENFORCEMENT=report   # report | strict | off

Tenant plan lifecycle

There is no Ductor-native plan-assignment API. Plan and subscription state flows one direction: provider → external_billing source → snapshot.

subscription state plan_tier + feature/limit items read plan_tier tier rank External billing external_billing source Entitlement snapshot plan_tier_at_least

Cancellation is observed via the provider webhook customer.subscription.deleted (detail on Provider integration). Separately, the reason code tenant_unprovisioned distinguishes a tenant that was never onboarded from one that was actively denied — the former is a grace admit, not a denial.

API surface

The billing summary is GET /api/v2/billing/summary (BillingService.GetBillingSummary), which requires tenant:read authorization. The EntitlementService endpoints that read and mutate sources and snapshots are owned by the entitlements docs — see Auth → Entitlements.