An OPE is a row in subs_object_products_element — an individual product component attached to a billable object (zone or account). Multiple OPEs compose a subscription. The parent table subs_object_products represents the billable object; each child OPE is a product component like zone service level, page rules count, or Workers usage.

SignalFieldValuesWhat It Tells You
Existencedeleted_dateNULL or timestampActive (NULL) vs soft-deleted / cancelled (set)
Current valuevalue_phases[0]"pro", 5, metered configWhat the customer has right now
Scheduled changevalue_phases[1]"free", -1, emptyWhat they'll have at next billing period (delayed downgrade)
Billing relationshiplicense_source_idsub_X:si_Y, ADMIN:reason, PENDING:PENDINGHow this OPE is billed — or if it's unbilled
In-flight operationworkflow_idEmpty or workflow IDWhether a Temporal workflow is pending (e.g. 3DS payment auth)
KindConstantDescriptionExample
enumObjectKindEnumDiscrete tier values"free", "pro", "biz", "ent"
sumObjectKindSumCountable quantitiesPage rules count, load balancer count
usageObjectKindUsageMetered usageWorkers requests, R2 storage
Stripe StatusCF StatusMeaning
activePaidSubscription is current and paid
trialingTrialIn a free trial period
incompleteAwaitingPaymentInitial payment not yet confirmed
past_dueExpiredRenewal payment failed, retrying
unpaidExpiredPayment retries exhausted
canceledCancelledSubscription terminated
incomplete_expiredCancelledInitial payment window expired
paused(unmapped)Not used in current flows
FormatMeaningBilled?
sub_XXX:si_YYYLinked to a Stripe subscription itemYes
ADMIN:reasonAdmin-provisioned (courtesy, migration)No
USER:reasonUser-provisioned (free trial)No
CONTRACT:CONTRACTBackfilled from enterprise contractVia contract
PENDING:PENDINGAwaiting Stripe subscription creationPending
Immediate Upgrade

Higher enum index or sum value → applied now. value_phases[0] updated, Stripe items modified, proration invoice generated, entitlements synced in the same request.

Delayed Downgrade

Lower enum index or sum value → deferred to period end. value_phases set to [current, future]. Stripe SubscriptionSchedule created with two phases. Customer retains higher tier until period ends.

On-Session 3DS

Change requiring Strong Customer Authentication → Temporal workflow created, workflow_id stored. If payment succeeds within 24h, workflow clears. If it fails or times out, changes are rolled back.

Orphaned Cleanup

Cron job (cleanup_orphaned_opes) runs periodically for OPEs with stale workflow_id values (>25h old). Classifies each as roll-forward, roll-back, or needs-manual-review based on Stripe invoice state.

ScenarioDirectionSpeed
Enum: higher index (e.g. free → pro)UpgradeImmediate
Enum: lower index (e.g. pro → free)DowngradeDelayed to period end
Sum: higher quantity (e.g. 3 → 5 page rules)UpgradeImmediate
Sum: lower quantity (e.g. 5 → 3 page rules)DowngradeDelayed to period end
Frequency: monthly → yearlyUpgradeImmediate
Frequency: yearly → monthlyDowngradeDelayed to period end
Deletion of base componentDowngradeDelayed (unless forced)
Sub-component follows baseFollows baseMatches base component speed
Forced immediate flag setEitherImmediate (overrides)
Unbilled / free provisionedEitherImmediate

Due to Stripe's classic billing mode, each PayGo customer has two Stripe subscriptions — one monthly, one yearly. The changes calculator routes each product change to the correct subscription based on billing frequency. Monthly covers Workers, R2, Stream, and usage-based products. Yearly covers zone plans (Pro/Biz/Ent), SSL, and annual add-ons. Cross-frequency moves require coordinated changes across both subscriptions.

When Stripe fires subscription events via webhooks, the provisioning system reconciles Stripe state with OPE rows and pushes updated entitlements. This is the reverse of the user-initiated flow — Stripe drives state back into subs-api. The most intricate part is detecting when a delayed downgrade has taken effect.

Step 1: Generate Candidates

Build multiple possible OPE configurations. Candidate 0 = no phase change (all use value_phases[0]). Candidate N = subscription N's OPEs advanced to value_phases[1]. Final candidate = all subscriptions advanced simultaneously.

Step 2: Test and Accept

For each candidate, match every Stripe subscription item against OPE rows. Accept the first candidate where all items are accounted for. If it's a phase-advanced candidate, update OPE rows. Error if no candidate matches.

Design decision: customer.subscription.deleted events are intentionally ignored. The subs-api database is the source of truth for cancellations — Stripe is a billing mechanism, not the authority on subscription existence. Cancellations flow from subs-api → Stripe, not the reverse.