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, migration 0012_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 pending to active. 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, then active → canceled | expired. canceled and expired are 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, planPaymentOutcome in packages/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(...) throws FeatureNotEntitledError (HTTP 402, code feature_not_entitled).
  • .getFeatureEntitlements(tenantId) returns { flows_custom_code: boolean }.
  • .getAddOnStatus(tenantId, "custom_code_steps") returns the AddOnStatus DTO.
  • 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:

  1. Open: active lines are added to the renewal invoice. Lines the customer cancelled move to canceled, with ended_at set to the period end, and are not billed again.
  2. Past due: the payment_failed email 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).
  3. Downgrade: after the grace period, the subscription and its active lines expire in one batch (billing.add_on.status_changed to expired). The downgraded email 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 var PLATFORM_OPERATOR_USER_IDS (comma- or space-separated usr_… 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).
  • reason must be 10–256 characters. expiresAt is 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_granted or billing.add_on.override_revoked to the project’s audit log. The actor_id is the operator, the metadata has actor_role: platform_operator, and it also records the add-on, the reason and the expiry.
  • Project members see override: { expiresAt } and source: override, never the reason.
  • Overrides work while BILLING_ENABLED is "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