AUTH4D-165 per-project add-ons: Custom code steps and the operator override
Flows custom code steps (option D) are off by default and sold per project as the Custom code
steps add-on. The add-on grants the feature entitlement flows_custom_code. A platform operator
can also enable it without payment.
Catalog (provisional)
The add-on catalog is in packages/billing/src/catalog.ts (ADD_ON_CATALOG). The client only
names an add-on id in the path. Price, interval and allowance always come from the server.
| Add-on id | Price (ZAR, integer cents) | Interval | Included executions | Grants | Plans |
|---|---|---|---|---|---|
custom_code_steps |
14900 (R149.00) |
month | 100,000 per month | flows_custom_code |
indie, studio |
The price and allowance are placeholders. The owner sets the final values before billing goes
live. Usage metering against the allowance belongs to the Flows tickets. This ticket only records
the allowance on the add-on line (included_executions).
Model
- An add-on is a line on the project’s paid plan subscription (
subscription_add_ons, migration0012_billing_add_ons.sql). Every row is keyed by(tenant_id, …), and child rows use composite tenant foreign keys. - Buying the add-on opens a prorated first invoice. It charges the monthly fee for the rest of
the current period, rounded up to the cent, with a minimum of R1.00 (the Stitch payment-link
minimum). When that invoice is paid, the line moves from
pendingtoactive. The plan period is not changed. - Each plan renewal bills active lines at their locked price, together with the plan. The renewal
invoice is itemised in
invoice_lines. Invoices without stored lines still report one plan line. - Line states are
pending → active | canceled | expired, thenactive → canceled | expired.canceledandexpiredare terminal. Only one line per add-on can be active (partial unique index). A second paid line that would duplicate an active one is canceled instead of activated. - Every status change goes through the billing state machine (
ADD_ON_TRANSITIONS,OVERRIDE_TRANSITIONS,planPaymentOutcomeinpackages/billing/src/state-machine.ts) inside guarded D1 batches. Each change emits a tenant audit event.
Entitlement: flows_custom_code
packages/tenancy/src/entitlements.ts exposes the entitlement:
createFeatureEntitlementService(db).hasFeature(tenantId, "flows_custom_code").assertFeature(...)throwsFeatureNotEntitledError(HTTP 402, codefeature_not_entitled)..getFeatureEntitlements(tenantId)returns{ flows_custom_code: boolean }..getAddOnStatus(tenantId, "custom_code_steps")returns theAddOnStatusDTO.resolveAddOnEntitlement(...)is the pure decision.
| Situation | enabled |
source |
|---|---|---|
| No line and no live override | false | none |
| Active line on an active plan in its paid period | true | subscription |
| Line cancelled by the customer, before the period end | true | subscription (endsAt set) |
| Plan renewal unpaid, within the 7-day dunning grace period | true | grace_period (graceEndsAt set) |
| Grace period over, or the plan ended | false | lapsed |
Live operator override (no expiry, or before expiresAt) |
true | override |
A paid entitlement is reported ahead of an override, and an override ahead of a grace period. Like
plan limits, the entitlement only gates management and feature configuration, never end-user
sign-in. The Flows tickets call assertFeature before saving or running a custom code step.
Lapse, grace and warnings
The renewal job (packages/billing/src/renewals.ts) handles lapses:
- Open: active lines are added to the renewal invoice. Lines the customer cancelled move to
canceled, withended_atset to the period end, and are not billed again. - Past due: the
payment_failedemail names the add-ons and the date they switch off (the end of the grace period). The console shows the same warning (source: grace_period). - Downgrade: after the grace period, the subscription and its active lines expire in one
batch (
billing.add_on.status_changedtoexpired). Thedowngradedemail reports the add-on as switched off.
Known limitation: a renewal paid after the downgrade reactivates the plan but not the expired add-on line. The customer buys the add-on again, or an operator issues an override or a refund.
Project routes
These routes are under /management/tenants/{tenantId}/billing:
| Route | Scope | Roles | Response |
|---|---|---|---|
GET /add-ons |
tenant:read |
any project member | ManagementBillingAddOnsResponse |
POST /add-ons/{addOnId}/checkout |
tenant:write |
owner, admin |
201 CheckoutSessionResponse (redirect URL only) |
POST /add-ons/{addOnId}/cancel |
tenant:write |
owner, admin |
ManagementBillingAddOnsResponse (cancels at period end) |
The checkout route shares the AUTH4D-40 checkout pipeline. It applies the same token, budget,
membership and role checks, needs BILLING_ENABLED, and requires an Idempotency-Key header of
16–128 token characters. It uses the same ledger and Stitch payment link. The key is pinned to
add_on:{addOnId}, so reusing a key for a plan or another add-on returns 422. The body must be a
JSON object, and nothing in it is read, so any client price is ignored. Status and cancel use D1
only and work while billing is off.
Add-on checkout errors (409) use fixed messages. The console never shows the API’s message text:
| Code | When |
|---|---|
no_paid_plan |
the project has no entitled plan subscription |
plan_not_eligible |
the plan is not in availableOnPlans |
subscription_past_due |
the plan renewal is overdue |
subscription_canceling |
the plan is set to cancel at period end |
subscription_period_ended |
the period ended and has not renewed yet |
add_on_active |
the add-on is already on (including a cancelled line still in its period) |
subscription_changed |
a concurrent change; retry |
Cancel returns 409 no_active_add_on when there is no paid line. A repeated cancel changes and
records nothing. A cancel by a viewer is audited as denied.
Operator override (control plane only)
Routes are under /management/operator/tenants/{tenantId}/billing/add-ons:
| Route | Scope | Body | Response |
|---|---|---|---|
GET / |
tenant:read |
— | statuses plus active overrides (with reason) |
PUT /{addOnId}/override |
tenant:write |
{ reason, expiresAt? } |
201 OperatorAddOnOverrideResponse |
POST /{addOnId}/override/revoke |
tenant:write |
{ reason } |
OperatorAddOnOverrideResponse |
- The caller presents a control-plane management token (customer tokens never reach
/management). Its user id must be listed in the Worker varPLATFORM_OPERATOR_USER_IDS(comma- or space-separatedusr_…ids). For anyone else, or when the var is unset, the routes answer 404. Project membership is not required. - The target must be an active customer project (otherwise 404).
reasonmust be 10–256 characters.expiresAtis optional and must be in the future. An expired override stops granting without any write.- Granting replaces an active override for the add-on: the old one is revoked in the same batch.
Revoking without an active override returns 404
no_active_override. - Every grant and revoke writes
billing.add_on.override_grantedorbilling.add_on.override_revokedto the project’s audit log. Theactor_idis the operator, the metadata hasactor_role: platform_operator, and it also records the add-on, the reason and the expiry. - Project members see
override: { expiresAt }andsource: override, never the reason. - Overrides work while
BILLING_ENABLEDis"false", so production can enable the add-on for beta users or as a comp before AUTH4D-135. - The console BFF forwards these routes for signed-in operators, as same-origin JSON writes.
Owner action: set PLATFORM_OPERATOR_USER_IDS per environment (it is a non-secret var) to the
control-plane user ids of the people allowed to issue overrides.
Audit events
billing.add_on.status_changed (with from, to and add_on_id), billing.add_on.cancel_requested,
billing.add_on.override_granted and billing.add_on.override_revoked. The add-on checkout also
records billing.checkout.created with add_on_id.
Console
The billing screen has an Add-ons section (apps/console/src/features/billing/add-ons.tsx). It
shows the price, the allowance and the status, plus an Enable custom code button (owners and
admins on a paid plan), cancellation, and the grace, override-expiry, cancellation and lapse
warnings. CustomCodeCallToAction is exported from the billing feature for the Flows screen. It
links to the billing page while the feature is off or in grace.
Tests
tests/integration/auth4d-165.add-ons.test.ts: end to end through the API composition and the mock Stitch server. Covers entitlement on, off, lapse after grace, the override (grant, expiry, revoke, billing off, non-operator 404) and tenant isolation.tests/integration/auth4d-165.add-on-units.test.ts: catalog and proration, the state machine, the entitlement decision, the email warnings, the console views and the BFF allowlist.
Source: docs/api-spec/auth4d-165.md