REFERENCE
The OPE Data Model
OPE (Object Products Element) — the atomic unit of subscription state. Subscription state is not a single status field. It is an emergent property of multiple signals read together. Covers composite state, value kinds, Stripe mappings, lifecycle, dual subscriptions, and provisioning reconciliation.
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.
| Signal | Field | Values | What It Tells You |
|---|---|---|---|
| Existence | deleted_date | NULL or timestamp | Active (NULL) vs soft-deleted / cancelled (set) |
| Current value | value_phases[0] | "pro", 5, metered config | What the customer has right now |
| Scheduled change | value_phases[1] | "free", -1, empty | What they'll have at next billing period (delayed downgrade) |
| Billing relationship | license_source_id | sub_X:si_Y, ADMIN:reason, PENDING:PENDING | How this OPE is billed — or if it's unbilled |
| In-flight operation | workflow_id | Empty or workflow ID | Whether a Temporal workflow is pending (e.g. 3DS payment auth) |
| Kind | Constant | Description | Example |
|---|---|---|---|
| enum | ObjectKindEnum | Discrete tier values | "free", "pro", "biz", "ent" |
| sum | ObjectKindSum | Countable quantities | Page rules count, load balancer count |
| usage | ObjectKindUsage | Metered usage | Workers requests, R2 storage |
| Stripe Status | CF Status | Meaning |
|---|---|---|
| active | Paid | Subscription is current and paid |
| trialing | Trial | In a free trial period |
| incomplete | AwaitingPayment | Initial payment not yet confirmed |
| past_due | Expired | Renewal payment failed, retrying |
| unpaid | Expired | Payment retries exhausted |
| canceled | Cancelled | Subscription terminated |
| incomplete_expired | Cancelled | Initial payment window expired |
| paused | (unmapped) | Not used in current flows |
| Format | Meaning | Billed? |
|---|---|---|
| sub_XXX:si_YYY | Linked to a Stripe subscription item | Yes |
| ADMIN:reason | Admin-provisioned (courtesy, migration) | No |
| USER:reason | User-provisioned (free trial) | No |
| CONTRACT:CONTRACT | Backfilled from enterprise contract | Via contract |
| PENDING:PENDING | Awaiting Stripe subscription creation | Pending |
Higher enum index or sum value → applied now. value_phases[0] updated, Stripe items modified, proration invoice generated, entitlements synced in the same request.
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.
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.
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.
| Scenario | Direction | Speed |
|---|---|---|
| Enum: higher index (e.g. free → pro) | Upgrade | Immediate |
| Enum: lower index (e.g. pro → free) | Downgrade | Delayed to period end |
| Sum: higher quantity (e.g. 3 → 5 page rules) | Upgrade | Immediate |
| Sum: lower quantity (e.g. 5 → 3 page rules) | Downgrade | Delayed to period end |
| Frequency: monthly → yearly | Upgrade | Immediate |
| Frequency: yearly → monthly | Downgrade | Delayed to period end |
| Deletion of base component | Downgrade | Delayed (unless forced) |
| Sub-component follows base | Follows base | Matches base component speed |
| Forced immediate flag set | Either | Immediate (overrides) |
| Unbilled / free provisioned | Either | Immediate |
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.
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.
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.
OPE state is composite, not singular. Five signals, three value kinds, dual subscriptions, and a hypothesis-testing reconciliation algorithm. The model is complex because the domain is complex — but the complexity is contained in one place.