The stratus monorepo houses three distinct checkout implementations. All converge at subscriptions-api through the API gateway — but through different HTTP clients, different Stripe integrations, and different state management patterns.

Unified Checkout react-query kumo-ui PaymentElement tailwind MODERN — per-product gates
Multisku Checkout redux component-payment-form CardElement 31-SKUs LEGACY — 31 products
subscriptions-ui ninjapanel admin-jwt TSSubscriptionService ADMIN ONLY — /api/v3/
fetch + React Query http-ts ApiError auto-unwrap checkout/api/http.ts
util-http + Redux makeActionCreator fetchWithoutErrorNotifications @cloudflare/util-http
Direct fetch + JWT admin-jwt proxy-api-v3 ADMIN-JWT auth
API Gateway route-mapping api-v4-prefix /api/v4/ → subscriptions-api
subscriptions-api changes-calculator billing-profile payment-methods BACKEND
React Query mutations Redux thunks TSSubscriptionService /api/v4/* /api/v4/* route to backend /api/v3/*
Unified Checkout
/:accountId/*/checkout

Modern stack. React Query + Stripe PaymentElement. Custom fetch wrapper with typed errors. Gated per-product.

Multisku Checkout
/:accountId/billing/checkout

Legacy stack. Redux + Stripe CardElement. 31 product SKUs. Three-view flow: review → payment → confirm.

subscriptions-ui
Internal ninjapanel

Internal admin tool. /api/v3/ with ADMIN-JWT auth. Admin-only actions: unban, sync-entitlements, lock/unlock.

Products are migrating individually from Multisku to Unified Checkout. Each product gets its own feature gate (e.g. billing-unified-checkout-load-balancer). When enabled, the product's checkout route renders UC instead of Multisku. The gate inventory controls which checkout system a customer sees for each product.

The Unified Checkout purchase flow traces five steps from product page to confirmation. Each step hands off to the next — cookie to nonce to API call to client secret to entitlement. The entire sequence is orchestrated by useSubmitPurchase.ts.

Product Page useCheckout startCheckout cookie-write TRIGGER
Payment Entry PaymentElement confirmPayment nonce STRIPE ELEMENTS
Item Separation subscriptions registrar single-update submitPurchase()
Stripe PaymentIntent SetupIntent 3DS-challenge EXTERNAL
On-Session Confirmation pi-confirmPayment seti-confirmSetup redirect-if-required pi_ vs seti_ BRANCHING
subscriptions-api bulk-subscriptions billing-checkout subscription-append BACKEND
Confirmation Page purchase-complete navigation DONE
Zone Initialization patchZonePending initializing-to-pending NEW ZONES ONLY
Entitlement Polling waitForEntitlements zone-activation account-entitlements ASYNC PROPAGATION
cookie nonce bulk/subs secrets[] confirm 3DS confirmed init? done
Step 1 — Cookie Handoff

Product page writes cart to cookie, navigates to checkout. No server-side session — cookie is the only state.

Step 2 — Payment Nonce

Stripe PaymentElement collects card/wallet. Frontend confirms to get a nonce — raw card data never touches our code.

Step 3 — Item Separation

Cart splits: subscriptions → bulkCreate, registrar → billingCheckout, single upgrades → updateSubscription.

Step 4 — On-Session Confirm

Each client_secret confirmed with Stripe. pi_ → confirmPayment, seti_ → confirmSetup. May trigger 3DS iframe.

Step 5 — Post-Purchase

Polls entitlements until active. Patches new zones from initializing → pending. Navigates to confirmation.

Key Constraint

user_is_on_session flag is transitional. After PM gate removal, client secrets become the default path.

The frontend never calls subscriptions-api directly. All API calls use relative URLs starting with /api/v4/ which hit the API gateway. The gateway routes to subscriptions-api endpoints grouped into three categories.

Stratus Frontend unified-checkout multisku-checkout ALL CALLERS
API Gateway route-rules v4-prefix /api/v4/ ROUTING
Purchase Endpoints bulk-subscriptions subscription-append billing-checkout RETURN secrets[]
Payment & Profile billing-profile payment-methods pre-auth CRUD + PM SETUP
Query Endpoints entitlements subscriptions-list READ ONLY
Admin (v3) subscriptions-ui admin-jwt NINJAPANEL
/api/v4/* POST CRUD GET secrets[] /api/v3/*
EndpointMethodReturnsCalled By
/bulk/subscriptionsPOSTclient_secrets[]UC + Multisku
/subscriptions/{id}/action/appendPOSTclient_secrets[]UC (upgrades)
/billing/checkoutPOSTclient_secrets[]UC (registrar)
/billing/profileGET/POST/PUTProfile objectUC + Multisku
/payment-methodsGET/POST/DELETEPM listUC + Multisku
/billing/profile/payment-methodPOSTStripe client secretUC (new PM)
/billing/pre_auth/stripe_payment_intentPOSTclient_secret + intent_typeUC (pre-auth)
/entitlementsGETEntitlement listBoth + product pages

Two HTTP client patterns coexist. The modern pattern was purpose-built for Unified Checkout. The legacy pattern predates React Query and uses Redux for all async state. Both prepend /api/v4 to all URLs. Error handling diverges completely.

Modern: fetch + React Query
checkout/api/http.ts

Native fetch() with auto-unwrap of {result: T}. Throws typed ApiError(status, errors[]). React Query for cache and retries.

Legacy: util-http + Redux
@cloudflare/util-http

makeActionCreator generates thunk triples. fetchWithoutErrorNotifications suppresses the red error bar — errors route through Redux instead.

Two distinct gates serve different purposes. The frontend billing-on-session-payment gate controls whether the dashboard handles payment confirmation via client secrets. The server-side PM gate at changes_calculator.go blocks subscription changes without a payment method on file. Their removal follows a strict dependency chain.

On-Session Gate billing-on-session-payment client-secret-flow FRONTEND GATE
Act 1 5-payment-paths confirm-before-commit ALL PATHS
PM Gate Removal changes-calculator HTTP-402 error-1210 SERVER-SIDE GATE
Deferred Collection subscribe-first client-secrets-default PM = SIDE EFFECT
Frontend 402/1210 ApiError-402 redux-error MUST COORDINATE
UC Full Rollout all-products one-surface TARGET STATE
enables all paths done gate deleted secrets = default 1210 consumers replace first

When the server-side PM gate fires, the frontend receives HTTP 402 with error code 1210. Both checkout systems handle this as a blocking error — but differently. Multisku suppresses the global error bar via fetchWithoutErrorNotifications and routes through Redux. UC catches it as an ApiError with status 402. Both paths must be updated before gate removal — after Deferred Collection, this error code should never appear.

GateTypeControls
billing-unified-checkoutFrontendMaster UC gate — enables Unified Checkout for eligible products
billing-on-session-paymentFrontend3DS/SCA confirmation flow via client secrets
billing-routesFrontendVisibility of the entire billing section at /:accountId/billing/*
billing-zone-creationFrontendZone creation flows
Per-product UC gatesFrontendIndividual product migration (e.g. billing-unified-checkout-load-balancer)
PM gate (402/1210)ServerBlocks subscription changes without a PM on file — Deferred Collection target