← StrategyPRD.md · draft v0.6 · 2026-06-27

PRD: Subscription Engine for BigCommerce

Status: Draft v0.6 Owner: Nino Chavez Last updated: 2026-06-27

Changelog

  • v0.6 (2026-06-27) — Governance-hygiene reconcile (synthesis #1818): header version corrected v0.2 → v0.6 to match changelog reality; Last-updated refreshed; the §6.3 Gateway Support Matrix changelog reference below fixed (was mislabeled §6.4 — the body has it at §6.3). Folds in the BC-platform shape corrections from the same research-red-team wave (synthesis #1814): removed the fabricated CATEGORIES value from the App-Extension model enum.
  • v0.5 (2026-05-16) — Corrected BC Payments backend identification: PPCP-powered (PayPal Commerce Platform; not Braintree-direct), per ADR-0037 + memory bc-payments-mit-verified. Architectural impact: zero (always pointed to right endpoint); descriptive accuracy: improved. Routes §1 Executive Summary + §3 Principles to match §6.2 Path A framing.
  • v0.3 (2026-04-21) — Added §16 Architectural Alternatives Considered (Stripe Billing weighed head-to-head against native engine) and §6.3 Gateway Support Matrix covering the full BC gateway universe.
  • v0.2 (2026-04-21) — BC Payments (launched March 2026, powered by PayPal/Braintree) promoted from Phase 3 to MVP-class processor; payment-vault constraint rewritten; competitive positioning sharpened around single-dashboard unification.
  • v0.1 (2026-04-21) — Initial draft.

1. Executive Summary

A BigCommerce-native subscription management platform installed as a marketplace app. Merchants enable subscriptions on existing BC products without duplicating catalog, orders flow through BC's native order system, renewal charges flow through BC Payments (the first-party PPCP-powered (PayPal Commerce Platform) rail launched March 2026) or Stripe, and a dedicated billing engine handles scheduling, dunning, and customer self-service. MVP targets parity with Recharge's core flows on BigCommerce while differentiating on three axes: BC-specific depth (App Extensions, Price Lists, Customer Groups, B2B Edition, Multi-Storefront), operational transparency (every recurring charge is observable, replayable, auditable), and direct integration with BC Payments — single admin dashboard, single payout flow, single payment relationship. Other BC subscription apps today run the payment relationship through a separate Stripe/Braintree account outside BC Payments.


2. Market Context

2.1 Competitive Landscape

Solution Positioning Platform Binding Billing Engine Catalog Model Main Weakness
Recharge Shopify-dominant, "Subscribe & Save" SMB/mid-market Shopify-first, BC via Headless Own engine + processor adapters (Stripe, Braintree, Authorize.net); BC Payments not a supported gateway (source) Variant-level attribute No App Extensions integration (Script Manager injection only — integration overview); price lists incompatible; no Multi-Storefront on BC Checkout Integration; separate processor required
Ordergroove Enterprise retention/reactivation Multi-platform (SFCC, BC, Magento, custom) Own engine Flexible model with predictive offers Enterprise-only pricing, heavy integration
Stripe Billing Payment-first subscription primitive Platform-agnostic Native (Stripe) Plans/prices in Stripe, not in commerce No catalog/fulfillment awareness — you build the commerce glue
Magento Commerce Subs Native module Magento only Vault-based, gateway-specific Product attribute Limited customer self-service
SAP Commerce Cloud Subs Enterprise B2B/B2C SAP only Native, ERP-integrated Plans + entitlements + usage Implementation complexity, $$$

2.2 The Gap in BigCommerce

BC merchants today pick between:

  • Recharge on BC — no App Extensions integration (documented BC integration uses Script Manager injection and product modifier manipulation only; no BC control panel App Extensions on Orders, Products, or Customers pages referenced in any Recharge integration documentation — Recharge integration overview); customer-group price lists explicitly incompatible with the Recharge Checkout (Recharge BC feature compatibility); no Multi-Storefront support on the BigCommerce Checkout Integration (Recharge BC Checkout feature status); requires a separate Stripe, Braintree, or Authorize.net relationship — BC Payments is not a supported gateway on any Recharge BigCommerce integration path (Getting started: Next steps).

    Unverified — confirm before citing externally: Search snippets attributed to docs.getrecharge.com indicate Recharge ended active BigCommerce support December 31, 2025 with full EOL October 2026. Static page fetch did not surface this text (consistent with a dynamic banner). Browser-render the canonical URL to confirm. If confirmed, the integration gaps above are secondary — the primary finding becomes: Recharge is sunsetting BigCommerce entirely.

  • Stripe Billing + custom glue — low platform fit, merchant operates two order systems and two payment relationships.

  • PayWhirl / Rebillia / MINIBC / Subscrimia — available on BC but architecturally diffuse: PayWhirl is Shopify-first by product architecture (dedicated Shopify app with Built-for-Shopify certification; BC handled under a multi-platform catch-all shared with Wix, Squarespace, and WordPress — PayWhirl BC setup); PayWhirl's BC-native customer account interface currently lacks a subscription section — customers manage subscriptions at a separate PayWhirl-hosted URL (PayWhirl's own docs: "the customer portal for BigCommerce doesn't yet have a subscription section"). Rebillia's entry tier ($49/mo, capped at 100 subscriptions) provides only a basic customer portal with no analytics; paid tiers ($180+/mo) unlock MRR, churn, LTV, and subscriber-growth dashboards and a full self-service portal (Rebillia pricing, Rebillia BigCommerce). PayWhirl, Rebillia, and MINIBC each require a separate processor relationship with no documented integration with BigCommerce Payments (PayWhirl gateway docs, MINIBC gateway list); Subscrimia's BC Payments compatibility has not been independently verified.

  • Build in-house — common at mid-market, but every merchant rebuilds the same billing engine.

There is no subscription product that is BC-first, mid-market-ready, and unified with BC Payments. That is the gap — and BC Payments' launch on March 31, 2026 made the gap demonstrably larger: every subscription app verified in this competitive scan — Recharge, PayWhirl, Rebillia, and MINIBC — requires a separate Stripe, Braintree, or Authorize.net processor relationship and has no documented integration with BC Payments, which launched with no published subscription or recurring billing support (BC Payments announcement). Merchants adopting BC Payments for their primary checkout today have no confirmed subscription app running inside that same payment relationship. We close that gap.


3. Strategic Positioning

Five pillars that define the product:

  1. BC-native depth. App Extensions on Orders, Products, and Customers pages. Price Lists drive subscription-only pricing. Customer Groups segment subscribers. Multi-Storefront/channel-aware from day one. B2B Edition support (approval workflows, purchase orders) by Phase 3.
  2. Orders are BC orders. Payments are BC Payments. Every recurring charge produces a native BC order and (for BC Payments merchants) settles through the merchant's BC Payments account into their BC Money dashboard. Inventory decrements, shipping engines, tax services, warehouse integrations, payouts, and reconciliation work through BC's existing surfaces rather than a parallel system. Other BC subscription apps today run the payment relationship through a separate Stripe/Braintree account outside BC Payments — a structural difference driven by product age, not ability.
  3. Primary processor is BC Payments; Stripe is the alternate path. Reversed from v0.1. BC Payments (PPCP rails underneath) ships as the primary processor adapter because it removes an onboarding step for merchants already on BC Payments — no separate Stripe Connect signup, no second payout, no second dashboard. Stripe remains a fully-supported adapter for merchants already on Stripe who do not plan to switch.
  4. Operationally observable. Every scheduled charge, retry, webhook, and order creation has a replayable audit record. Merchants can see why a renewal failed without opening a support ticket.
  5. Headless-ready from MVP. Customer portal ships as both (a) hosted BC-iframe widget and (b) headless SDK for Catalyst / custom storefronts. No retrofit later.

4. Non-Goals (MVP)

Declaring these up front to bound scope:

  • Not a general billing platform. No standalone invoicing, no B2B SaaS billing, no general-purpose metering-as-a-service. Usage-based subscription pricing (consumption × rate, US-6.6) is in scope; arbitrary billing-platform metering is Stripe Billing's job.
  • Not a loyalty / rewards engine. Subscriptions may earn rewards through third-party loyalty apps; we do not build the rewards system.
  • Not a CRM or marketing automation platform. We emit events; Klaviyo/Mailchimp/Ortto consume them.
  • Not a custom checkout. MVP uses BC's native checkout with a subscription-capture layer; we do not replace Optimized One-Page Checkout.
  • Not a tax or shipping calculation engine. We delegate to BC's native tax (BC + Avalara/TaxJar) and shipping rules.
  • Not a payment processor. We integrate with processors; we do not store PCI-scope card data ourselves.

5. Personas

5.1 Merchant Admin (primary buyer + daily user)

  • Runs a BC store doing $1M–$50M GMV
  • Sells CPG, vitamins, coffee, pet food, or consumables
  • Has existing BC catalog and payment processor — usually BigCommerce's auto-enabled PayPal-powered-by-Braintree default, with Stripe common upmarket and the rest a long tail (the precise processor split is an internal [assumption], not sourced market data — see #1581)
  • Pain: Recharge's BC port feels bolted on; reconciliation between Recharge orders and BC orders is painful; Shopify-speak in the UI doesn't match BC concepts.
  • Success: "Every subscriber looks like a regular BC customer with a recurring order rhythm."

5.2 Subscriber (end consumer)

  • Logged-in BC customer
  • Expects: skip next order, swap a product, change cadence, pause, update payment, reschedule — all from one portal, zero support tickets
  • Pain: most BC subscription portals are 2010-era tables
  • Success: portal that feels native to the storefront brand

5.3 Developer (implementation partner + in-house)

  • BC agency partner or mid-market in-house dev
  • Needs: headless SDK, webhooks, stable API, migration path from legacy subscription apps
  • Success: ships a custom subscriber portal in <2 weeks

5.4 Support / Ops (merchant side)

  • Reconciles failed renewals, handles cancel requests, processes refunds
  • Needs: one screen to see subscriber, upcoming charge, last 5 orders, payment method health, dunning state
  • Success: resolves failed renewal without switching between 3 tools

6. Platform Constraints (BC Reality Check)

Honest inventory of what BC gives us and what it does not — this drives the architecture.

6.1 What BC provides that we use

  • Admin APIs (V3/V2): Products, Variants, Customers, Orders, Carts, Price Lists, Customer Groups, Channels, Webhooks, Store Information.
  • Storefront GraphQL API: product catalog, cart mutations — used by headless subscriber portal.
  • App Extensions (GraphQL): PANEL or LINK context on PRODUCTS, PRODUCT_DESCRIPTION, ORDERS, and CUSTOMERS — BC's documented model enum is exactly these four. Neither CARTS nor CATEGORIES is a supported model; cart-side merchant UX must use a different surface (Stencil widget, Storefront Scripts API, or a customer-page deep-link).
  • Webhooks (registered): store/order/created, store/order/updated, store/cart/created, store/cart/updated, store/cart/deleted, store/customer/updated, store/product/updated, store/product/deleted. store/app/uninstalled is auto-fired by BC on uninstall and does not require explicit subscription registration.
  • Custom Fields + Metafields: Attach subscription configuration to products.
  • Price Lists: Subscription-only pricing (e.g., "Subscribe & Save 10%").
  • Customer Groups: Segment subscribers for pricing, shipping, tax rules.
  • Channels / Multi-Storefront: Scope subscriptions to specific sales channels.
  • Scripts API / Stencil: Inject subscription widgets on PDP/cart.
  • B2B Edition APIs (Phase 3): Company accounts, buyer hierarchies, approval workflows.

6.2 What BC does not provide — and how we work around it

The payment vault landscape (vault-based rail per ADR-0037).

BC's stored-instruments vault is a gateway-agnostic recurring-charge rail (ADR-0037): any BC-supported gateway with "Stored Payments" enabled routes charges through payments.bigcommerce.com, with no gateway-specific tokenization SDK required in our subscription engine. At standard Stencil/Catalyst checkout, the shopper enters card data into BC's hosted checkout UI; BC/BigPay internally sets vault_payment_instrument: true on the payment request, vaults the card with the provider, and returns an opaque bigpay_token. Our app never receives raw card data and makes no Stored Instruments API call at checkout — we harvest the vault token via GET /v3/orders/{id}/transactions in the store/order/created webhook handler. (Instrument Access Token (IAT) direct-entry applies only to our portal add-payment-method flow — BRD US-19.1 — where the shopper adds a new card from within our portal UI.) Three charge paths remain, differing by whether the merchant uses BC's vault or a direct-processor connection:

Path A — BC stored-instruments vault (primary MVP path, canonical per ADR-0037). Merchant uses any BC-supported gateway with "Stored Payments" enabled (BC Payments/PPCP, Braintree, Auth.Net, and others). Card-on-file is vaulted at checkout by BigPay — when the shopper ticks "save for future use", BC's hosted checkout UI sets vault_payment_instrument: true on the BigPay payment request. BigPay executes a vault-and-pay (or pay-and-vault, or vault-via-notification) flow with the provider, stores provider tokens, and returns a bigpay_token. Our app harvests this token from GET /v3/orders/{id}/transactions in the store/order/created webhook — no IAT, no direct Stored Instruments API call from our app at checkout. (The scopes store_payments_access_token_create and store_stored_payment_instruments power our S2S MIT renewals and the portal add-PM flow respectively — not checkout vaulting.) For recurring off-session renewals, we charge the stored instrument via:

  • Canonical three-call sequence: (1) POST /v3/payments/access_tokens to mint a Payment Access Token (is_recurring: true); (2) GET /v3/payments/methods?order_id=N to discover the stored-instrument token; (3) POST payments.bigcommerce.com/stores/{hash}/payments to execute the charge. Charges are auto-classified MIT by BC's gateway abstraction (MREC flag for recurring renewals — see §6.3.1). Validated by Day-0 spike eb94cf4a against internal Confluence + Wren Laboratories production telemetry.
  • Fallback (capability, never primary): direct Braintree SDK charge using the Braintree payment-method token surfaced via Stored Instruments. Maintained as adapter capability for edge cases; not a documented production target.

Worldpay/Paymetric NTI note (non-blocking per ADR-0037): BC's network_transaction_id threading has a gap for Worldpay/Paymetric merchants (~4358 instruments empty across top 3 stores). Tracked by PI-5062. This does NOT block standard merchant adoption — BC Payments/PPCP, Braintree, and Auth.Net work without it. Until PI-5062 closes, our adapter persists NTI from the CIT response and re-attaches on every MIT for Worldpay/Paymetric stores (see PRD-COMPANION D17).

Settlement lands in the merchant's BC-connected gateway account and surfaces in BC's native reporting. Zero external processor relationship for vault-path merchants. This is the differentiator.

Path B — Stripe (alternate MVP path). Merchant is on Stripe — a material secondary gateway in our target segment, with first-class off-session/MIT primitives. We onboard them via Stripe Connect, vault the PM at Stripe (payment_method attached to a Stripe Customer), and charge Stripe directly on renewal with an idempotency key. Settlement lands in the merchant's Stripe account, not BC Payments.

Path C — Other (Phase 2). Braintree standalone, Authorize.net, adyen, etc. Clean adapter interface already exists once Paths A + B are proven.

The data model rule is the same across paths: subscription holds a payment_method_ref + processor_connection_id. The adapter abstracts gateway specifics. On successful charge, we create the BC order with payment_status: captured and the processor transaction ID.

Multi-phase capture timing (ADR-0038). The three-call charge sequence above is atomic (authorization + capture in one call) in Phase 1 — correct for digital goods and immediate-fulfillment merchants. Physical-goods merchants (EU PSD2 compliance, warehouse deferred-capture workflows) need capture deferred to shipment or fulfillment confirmation. Phase 1 ships a stores.capture_timing column (immediate | on_fulfillment | on_ship, default immediate) as advisory configuration with no behavior change. Phase 2 wires the actual deferred semantics by splitting adapter.charge() into adapter.authorize() + adapter.capture(), with store/shipment/created and store/order/statusUpdated webhook handlers driving capture events, and an auth-expiry sweep reaping stale authorizations nearing gateway auth-window limits. See ADR-0038 for the full deferred-build spec; see STATE.md R-12/R-13 for the open compliance risk until Phase 2 ships. BRD US-10.9 + US-2.7 track the user-story AC.

Why Path A is the primary MVP path (reversed from v0.1):

  • New BC merchants signing up in 2026+ default to BC Payments — lowest install friction.
  • Existing PPCP merchants have a BC-driven migration path to BC Payments; we ride that wave.
  • Eliminates Stripe Connect onboarding step from our own install flow.
  • Single payment dashboard is a concrete UX differentiator over every competitor on BC (none of which currently integrate with BC Payments).

Other gaps:

BC Gap Our Approach
No native recurring order model Store subscription state in our Postgres; generate BC orders on schedule
No native subscription product type Product becomes "subscribable" via metafields + Price List assignment
No native dunning / retry logic Our billing engine owns retry state machine
No native pause/skip primitive Our domain model; exposes via API + portal
No native subscriber portal We provide both hosted and headless
Orders API can't attach "recurring" metadata natively Use order custom_fields + order metafields to tag source subscription
No native prepaid / fixed-term subscriptions Modeled as pre-computed charge schedule in our engine

6.3 Gateway Support Matrix

Note (synthesis 7574bb48-c8f0-4943-aed4-19ecb2e5584e): §6.3.1 below is the new "Stored Credential Indicators" subsection added by this synthesis. The gateway support matrix that follows is unchanged.

BC supports ~65 gateways. Not all expose the primitives a subscription engine needs (vault, MIT/off-session charge, stable webhooks, refund API). BC's own Stored Instruments API is gateway-gated — per BC docs: "Not all gateways support processing a payment using both stored payment instruments and raw card data." Current BC docs specifically name PayPal Powered By Braintree and PayPal (Commerce Platform) as vault-capable for stored PayPal accounts via this API.

Our explicit support posture:

Gateway Region Vault MIT / Off-session Our MVP Notes
BC Payments (PayPal/Braintree) US (expanding) Yes, via BigPay vault layer + Braintree provider tokens; managed S2S via Stored Instruments API Yes (via Braintree; BC API surface TBC — see §13) Yes (primary) Only path unified with BC Money dashboard
Stripe Global Yes (Stripe Customer + PaymentMethod) Yes (PaymentIntent off_session:true) Yes (alt) Material secondary gateway; first-class off-session/MIT
Braintree (standalone, non-BC-Payments) Global Yes Yes (Braintree Transaction API) Phase 2 Redundant for BC Payments merchants; needed for legacy Braintree merchants who haven't migrated
Authorize.net US/CA Yes (CIM) Yes Phase 2 Long tail of older merchants
Adyen Global (strong EU) Yes Yes Phase 2 Critical for EU mid-market; hold until Phase 2 because adapter work is non-trivial
Cybersource Global Yes Yes Phase 2 Common in enterprise
Worldpay / FIS UK/EU Yes Yes Phase 2 UK mid-market
Checkout.com Global Yes Yes Phase 2 EMEA merchants
Square US/AU/UK/CA Yes (Cards API) Limited Phase 3 Off-session posture less mature
eWAY AU/NZ Yes Yes Phase 3 AU merchants
Klarna / Afterpay / Clearpay Global N/A (BNPL) No (regulatory) Never Structurally incompatible with unattended recurring charges
Amazon Pay US/EU/JP No (session-bound) No Never Not designed for MIT
Apple Pay / Google Pay (as wallet passthrough) Global Via underlying card Depends on processor Inherits from underlying processor Wallet is a presentment layer, not an adapter
All other gateways Various Varies Varies Not supported Document explicitly; merchant must switch processor to subscribe

Merchant-facing consequence: during install, we detect active gateways on the store and show a compatibility badge. If the merchant's gateway is not supported, we offer three paths: (1) switch to BC Payments (one click if eligible), (2) add Stripe as a secondary processor, (3) decline install. No silent degradation.

6.3.1 Stored Credential Indicators

Card networks classify stored-credential transactions into subtypes. Each gets different fraud, SCA, and authorization-rate treatment. Sending the wrong subtype on a recurring renewal causes 3–8% decline-rate degradation and forfeits EU PSD2 RTS Art. 18(b) recurring exemption — 3DS challenges fire off-session and the charge declines.

BC's flag taxonomy (per Day-0 spike eb94cf4a, Confluence 2671869953):

Cardholder-initiated Merchant-initiated
CSTO — initial vault MUSE — non-recurring MIT (one-off)
CUSE — using stored card MREC — recurring renewal (our primary)
CREC — initial subscription cycle (cardholder triggers)
CINS — installments (not applicable in BC)
CGEN — plain checkout

Our usage:

  • Default plan model (fixed-interval/fixed-amount): every renewal sends MREC. Required for EU SCA exemption.
  • Usage-based plans (Phase 2+): unscheduled-COF subtype.
  • Chain position: captured per-charge in charges.chain_position ('initial' | 'subsequent'). Determines the indicator sent on each MIT.
  • Network transaction ID: persisted on payment_methods.network_transaction_id from the CIT-first auth, re-attached on every subsequent MIT to anchor the recurring chain. Required for SCA exemption preservation across the chain.

The ProcessorAdapter.charge() signature carries this context per BRD US-10.2:

processorAdapter.charge({
  idempotencyKey: string,
  amount: Money,
  paymentMethodRef: string,
  mitContext: {
    type: 'recurring' | 'unscheduled' | 'installment',
    chainPosition: 'initial' | 'subsequent',
    networkTransactionId: string | null  // null only on chainPosition='initial'
  }
})

Per-adapter mapping:

Adapter mitContext.type=recurring maps to
BC Payments MREC flag in BigPay payload + persisted NTI re-attached
Stripe payment_method_options.card.mit_exemption.recurring=true + previous_network_transaction_id
Braintree (standalone) transactionSource: 'recurring' + externalVault.previousNetworkTransactionId

6.4 Cart/Checkout capture constraint

BC's Optimized One-Page Checkout does not expose server-side hooks to capture "this item is a subscription" metadata. Two viable paths:

  1. Cart-level metafields: Storefront widget writes subscription intent into cart-level metafields (namespace: bc-subscriptions, key: subscription_intents); the store/order/created webhook handler fetches them via GET /v3/carts/{cartId}/metafields and creates the subscription. (BC's cart API has no line-item-level custom_fields extension surface.)
  2. Post-purchase hook: Thank-you page JavaScript posts subscription intent; resilient but not atomic with order creation.

MVP uses approach 1. Approach 2 exists as fallback for Stencil/legacy storefronts that cannot render the widget in-cart.


7. Architecture

7.1 App Type

Marketplace-installed app (following the ask-bc / aisles-admin pattern):

  • OAuth 2.0 install → Upstash Redis–backed encrypted credential store
  • Control-panel iframe via signed_payload_jwt on load
  • App Extensions registered via GraphQL on install (Orders, Products, Customers panels)
  • Webhook subscriptions registered on install
  • Uninstall revokes credentials and deregisters webhooks

7.2 Runtime Layout

┌──────────────────────────────────────────────────────────────────┐
│ Merchant UI (BC Admin iframe)       Subscriber Portal            │
│ Next.js + BigDesign                 Next.js + headless SDK       │
│ /stores/[storeHash]/*               /portal/*                    │
└──────────────────────────────────────────────────────────────────┘
                │                              │
                └──────────────┬───────────────┘
                               │
┌──────────────────────────────▼───────────────────────────────────┐
│ API Layer (Next.js API routes on Vercel Fluid Compute)           │
│ - OAuth, session, App Extension load                             │
│ - Merchant CRUD (plans, subs, exceptions)                        │
│ - Subscriber portal API                                          │
│ - Webhook receivers (BC → us, processor → us)                    │
└──────────────────────────────┬───────────────────────────────────┘
                               │
┌──────────────────────────────▼───────────────────────────────────┐
│ Billing Engine (Vercel Queues + Cron)                            │
│ - Scheduler: nightly scan for charges due in next 24h            │
│ - Executor: charge processor → create BC order → emit events     │
│ - Dunning: tunable retry state machine (default: 5-stage)        │
│ - Reconciliation: detect drift between us ↔ BC ↔ processor       │
└──────────────────────────────┬───────────────────────────────────┘
                               │
┌──────────────────────────────▼───────────────────────────────────┐
│ Data Layer                                                        │
│ Postgres (Neon):  subscriptions, plans, charges, events           │
│ Redis (Upstash):  BC credentials, idempotency keys, rate limits   │
│ Vercel Blob:      subscriber-facing exports (CSV)                 │
└──────────────────────────────────────────────────────────────────┘
                               │
┌──────────────────────────────▼───────────────────────────────────┐
│ External Systems                                                  │
│ BigCommerce APIs (V2/V3/GraphQL/Storefront)                      │
│ Stripe (MVP) / Braintree / Auth.net (Phase 2)                    │
│ Email (Resend) / SMS (Twilio)                                    │
└──────────────────────────────────────────────────────────────────┘

7.3 Stack (matches existing workspace conventions)

  • Framework: Next.js 16 App Router (matches aisles-admin precedent — React 19, BigDesign v2)
  • Language: TypeScript 5
  • UI (admin): @bigcommerce/big-design v2, tw- prefixed Tailwind
  • UI (portal): Plain Tailwind, headless components, brand-theme–driven
  • Data: Neon Postgres + @neondatabase/serverless driver
  • Cache/ephemeral: Upstash Redis
  • Jobs: Vercel Queues + Vercel Cron (durable, at-least-once)
  • Auth: BC signed_payload_jwt → internal JWT (jose) with partitioned cookies (iframe)
  • Env: @t3-oss/env-nextjs + Zod v4 validation
  • Config: vercel.ts (per current platform guidance)

8. Data Model

8.1 Core Entities

<!-- traceability:start:PRD:8.1 -->

Prototype: Membership · Product Panel · Plan Wizard · Plan Activated

<!-- traceability:end:PRD:8.1 -->
Store                                             one per installed BC store
  store_hash (PK)
  access_token_encrypted
  installed_at, uninstalled_at
  settings (JSONB: default_retry_policy, currency, timezone, notification_prefs)

Processor Connection                              one-to-many store → processor
  id (PK)
  store_hash (FK)
  processor (enum: stripe, braintree, authnet, bc_payments)
  account_ref (Stripe account, Braintree merchant, etc.)
  credentials_encrypted
  is_default

Plan                                              subscription configuration template
  id (PK)
  store_hash (FK)
  bc_product_id                                   link to BC product
  bc_variant_id (nullable)                        null = all variants eligible
  name, description
  interval_unit (enum: day, week, month)
  interval_count (int)                            e.g., 2 weeks = unit=week, count=2
  available_intervals (JSONB)                     offered choices: [{unit, count, label}]
  pricing_strategy (enum: bc_price_list, fixed_discount_pct, fixed_price)
  bc_price_list_id (nullable)                     FK to BC Price List
  discount_pct (nullable)
  trial_days (nullable)
  min_cycles (int, nullable)                      prepaid / commitment
  max_cycles (int, nullable)
  status (enum: active, archived)

Subscription                                      active subscriber agreement
  id (PK)
  store_hash (FK)
  bc_customer_id (FK)
  plan_id (FK)
  bc_variant_id
  quantity
  interval_unit, interval_count
  status (enum: active, paused, past_due, cancelled, completed)
  next_charge_at (timestamptz)
  anchor_date                                     for cadence math
  processor_connection_id (FK)
  payment_method_ref                              Stripe PM ID, Braintree token, etc.
  shipping_address (JSONB snapshot)
  billing_address (JSONB snapshot)
  created_at, paused_at, cancelled_at
  metadata (JSONB)                                merchant-defined tags

Charge                                            one attempt to collect one renewal
  id (PK)
  subscription_id (FK)
  scheduled_at, attempted_at, resolved_at
  amount_cents, currency
  status (enum: scheduled, processing, succeeded, failed, abandoned, refunded)
  processor_transaction_id
  bc_order_id                                     set once BC order is created
  failure_code, failure_message
  retry_attempt (int, default 0)
  next_retry_at

Event                                             append-only audit log
  id (PK)
  store_hash (FK)
  subscription_id (FK, nullable)
  charge_id (FK, nullable)
  type (text)                                     sub.created, charge.succeeded, dunning.attempt_2, etc.
  source (enum: merchant, subscriber, system, webhook_bc, webhook_processor)
  actor_id                                        who triggered it
  payload (JSONB)
  created_at

Entitlement                                       access granted by a subscription, independent of physical fulfillment
  id (PK)                                         (PRD-COMPANION D1, DECIDED 2026-04-23)
  store_hash (FK)
  subscription_id (FK)
  key (text)                                      merchant-defined entitlement key (e.g., "gold_tier", "seat_pro", "course_access")
  status (enum: pending, active, suspended, revoked)
  provider (text)                                 adapter id: bc_customer_group (v1) | sso_saml | feature_flag | license_server | custom
  provider_external_ref (text, nullable)          opaque reference from the provider (BC customer_group_id, SSO attr, etc.)
  granted_at (timestamptz, nullable)
  suspended_at (timestamptz, nullable)            set when payment.failed + grace exhausted or operator-suspended
  revoked_at (timestamptz, nullable)              set when subscription.cancelled + cancel-grace expires
  metadata (JSONB)                                adapter-specific state (role name, SSO assertion attrs, feature-flag targets)

AllotmentGrant                                    admin-granted recurring quota distinct from paid-subscription entitlement
  id (PK)                                         (PRD-COMPANION D18, DECIDED 2026-05-07)
  store_hash (FK)
  bc_customer_id (FK, nullable)                   target customer (null + buyer_org_ref non-null = org-scoped grant)
  buyer_org_ref (text, nullable)                  B2B Edition buyer-org id; mutually exclusive with bc_customer_id
  grant_key (text)                                merchant-defined wallet name (e.g., "wellness_credit", "shred_pickup_quota")
  unit_type (enum: currency, count, duration_minutes)
  amount_per_period (numeric)                     refresh quantum; e.g., 50.00 USD or 4 pickups
  currency (text, nullable)                       set when unit_type=currency
  refresh_cadence_unit (enum: day, week, month, year)
  refresh_cadence_count (int)
  next_refresh_at (timestamptz)
  current_balance (numeric)                       depleted by AllotmentDebit, refreshed at next_refresh_at
  rollover_policy (enum: none, cap_to_one_period, accumulate)
  status (enum: active, suspended, revoked)
  granted_by (text)                               admin user id who issued the grant
  reason (text, nullable)                         audit context (CS-ticket ref, contract clause)
  expires_at (timestamptz, nullable)              fixed end-of-grant; null = perpetual until revoke
  metadata (JSONB)

AllotmentDebit                                    consumption events against an AllotmentGrant
  id (PK)
  grant_id (FK)
  bc_order_id (text, nullable)                    if debit funds an order
  charge_id (FK, nullable)                        if debit offsets a subscription charge
  amount (numeric)
  occurred_at (timestamptz)
  reversed_at (timestamptz, nullable)             refund / void
  notes (text, nullable)

Actor                                             multi-actor membership on a subscription (PRD-COMPANION D19)
  id (PK)                                         (DECIDED 2026-05-07)
  subscription_id (FK)
  store_hash (FK)
  bc_customer_id (FK, nullable)                   resolved BC customer (null when role is platform-managed e.g. CS-rep)
  buyer_org_ref (text, nullable)                  B2B Edition org context
  role (enum: owner, payer, beneficiary, manager, org_admin)
  payment_method_ref (text, nullable)             populated when role=payer; supersedes Subscription.payment_method_ref
  processor_connection_ref (FK, nullable)         RESERVED for v2 marketplace MoR (ADR-0022); null in Phase 1
  notification_prefs (JSONB)                      per-actor channel routing
  added_at, removed_at (timestamptz)
  added_by (text)                                 audit
  CONSTRAINT exactly-one-payer-per-subscription   active payer is unique per subscription_id

DeliveryInstance                                  per-delivery row, distinct from per-billing charge (ADR-0024)
  id (PK)                                         (PRD-COMPANION D2, DECIDED 2026-05-07)
  subscription_id (FK)
  store_hash (FK)
  scheduled_date (date)                           when this delivery is/was supposed to ship
  status (enum: pending, fulfilled, skipped, failed, rescheduled)
  ship_to_address_id (uuid, nullable)             null = subscription default; non-null = per-instance override
  line_items (JSONB, nullable)                    null = subscription default; non-null = per-instance swap
  bc_order_id (text, nullable)                    set when materialized to a BC order
  source_charge_id (FK, nullable)                 informational; n:1 fan-out permitted (Cintas weekly delivery / monthly billing)
  notes (text, nullable)
  created_at, updated_at (timestamptz)

CustomFieldDefinition                             merchant-defined typed extensibility on Subscription (PRD-COMPANION D20)
  id (PK)                                         (DECIDED 2026-05-07)
  store_hash (FK)
  scope (enum: subscription, plan, allotment_grant)
  field_key (text)                                snake_case identifier, unique per (store_hash, scope)
  display_label (text)
  field_type (enum: text, number, boolean, date, enum, json)
  enum_values (JSONB, nullable)                   choices when field_type=enum
  required (boolean)
  default_value (JSONB, nullable)
  visible_to_subscriber (boolean)                 controls portal display
  editable_by_subscriber (boolean)
  validation_regex (text, nullable)
  description (text, nullable)
  status (enum: active, archived)
  created_at, updated_at (timestamptz)

  -- field data lives on the parent entity's `metadata` JSONB column under reserved key `custom_fields`
  -- e.g. subscriptions.metadata = { "custom_fields": { "po_number": "PO-12345", "delivery_window": "morning" } }

Dunning Policy                                    retry strategy per store
  id (PK)
  store_hash (FK)
  attempts (JSONB)                                [{delay_hours: 12}, {delay_hours: 12}, {delay_hours: 24}, {delay_hours: 48}, {delay_hours: 72}]
  on_exhaustion (enum: cancel, pause, notify_only)
  is_default

8.2 Storage Strategy — where each piece lives

Data Lives In Why
Subscription state, schedule, cadence Our Postgres BC has no native representation
Plan configuration Our Postgres + BC product metafields (pointer back) Merchant can see "this product has a subscription plan" inside BC admin via App Extension
Customer payment method Processor (Stripe/Braintree) PCI scope; we hold only a reference
Customer profile BC Customers (source of truth) Don't duplicate
Product catalog BC Products (source of truth) Don't duplicate
Order history BC Orders (source of truth) We tag orders via custom fields; don't duplicate
Upcoming-charge schedule Our Postgres Derived from subscription + anchor_date
Audit trail Our Postgres (events table) Needed for support and compliance
Multi-actor roles on a subscription Our Postgres (actors table) BC customer = identity SoR; role assignment is our concept
Per-delivery instances (cadence ≠ billing) Our Postgres (delivery_instances) Lazily populated; BC has no native concept (ADR-0024)
Admin-granted recurring quota Our Postgres (allotment_grants + allotment_debits) Independent of paid-subscription entitlement (ADR-0024 sibling, PRD-COMPANION D18)
Custom field definitions Our Postgres (custom_field_definitions) Merchant-managed schema; data values live on parent entity metadata.custom_fields
Per-org processor connection (v2) Our Postgres (processor_connections keyed on payer_org) RESERVED in Phase 1 (ADR-0022 marketplace-MoR deferral); enabled at v2 with backfill

8.3 Linking BC Orders to Subscriptions

Every subscription-generated BC order gets:

  • Order custom_fields entries: {subscription_id, charge_id, cycle_number}
  • Order staff_notes prefix: [SUB] {subscription_id}
  • Webhook handler on store/order/* reverse-looks-up by custom_fields to maintain sync

For subscriptions with delivery cadence ≠ billing cadence (Cintas / Lovesac patterns), order custom_fields additionally carry {delivery_instance_id} so the reconciliation worker can close per-instance loops independent of the funding charge (ADR-0024).

8.4 Cross-references to ADRs and PRD-COMPANION

The Phase-1 entity additions above are governed by these ratified decisions — refer to those documents for context, alternatives, and consequences:

  • ADR-0022 (docs/decisions/0022-marketplace-mor-scope.md) — defers per-buyer-org processor routing to v2; Actor.processor_connection_ref is the reserved seam.
  • ADR-0023 (docs/decisions/0023-b2b-checkout-ownership.md) — admin-only B2B Edition enrollment in Phase 1; Actor.role=manager carries CS-rep semantics.
  • ADR-0024 (docs/decisions/0024-delivery-instance.md) — DeliveryInstance as first-class lazily-populated entity; n:1 charge-to-delivery fan-out permitted.
  • PRD-COMPANION D2 (DECIDED) — DeliveryInstance shape (this section's row).
  • PRD-COMPANION D18 (DECIDED) — AllotmentGrant primitive distinct from Entitlement (D1).
  • PRD-COMPANION D19 (DECIDED) — Multi-actor role enum + cardinality constraints.
  • PRD-COMPANION D20 (DECIDED) — CustomFieldDefinition schema + storage location.
  • PRD-COMPANION D21 (DECIDED) — NTI freshness policy (Phase 2 charge-orchestration extension; new fields on Subscription deferred to that decision's adoption).

9. Feature Scope

9.1 Catalog Enablement (merchant side)

Goal: Any existing BC product can become subscribable without duplicating SKUs.

Flows:

  • Merchant opens a product in BC admin → "Subscriptions" App Extension panel appears
  • Panel shows: current plan status, intervals offered, pricing strategy, eligibility
  • "Enable subscription" → wizard:
    1. Pick interval(s) to offer (e.g., monthly, every 2 months)
    2. Pick pricing: BC Price List link or fixed discount % or fixed price
    3. Optional: trial, commitment cycles, max cycles
    4. Pick eligible variants (all / specific)
    5. Review → activate
  • On activation: we write metafields to the BC product (subscription.enabled=true, subscription.plan_id=...) so the storefront widget knows to render the subscription UI

Price List integration (differentiator):

  • Merchant selects an existing BC Price List → subscription price comes from there
  • Lets merchants reuse B2B / customer-group pricing structures for subscribers
  • Maintains single source of pricing truth (no divergent subscription prices to reconcile)

9.2 Storefront Purchase Flow

Widget on PDP (Stencil + Headless):

  • Radio: "One-time purchase" / "Subscribe & save X%"
  • Dropdown: interval selection (when multiple offered)
  • Writes subscription intent into cart-level metafields (namespace: bc-subscriptions, key: subscription_intents) on add-to-cart
  • Renders badge + next-charge preview

Cart page:

  • Subscription line items visually distinguished (badge, interval label)
  • Non-subscribable items and subscribable items can coexist in one cart

Checkout:

  • BC Optimized One-Page Checkout runs normally
  • Our webhook on store/order/created reads line-item subscription intent, creates Subscription records, captures payment method at processor (see 9.3)

Headless Catalyst path:

  • Same backend; SDK provides useSubscriptionOptions(productId) and addSubscriptionToCart()
  • PDP, cart, checkout are all merchant-built against our SDK

9.3 Billing Engine

Scheduling:

  • Vercel Cron runs every 15 min
  • Query: SELECT * FROM charges WHERE status='scheduled' AND scheduled_at <= now() + interval '15 min'
  • Enqueue each due charge into Vercel Queue for workflow-style execution

Executor (Vercel Workflow):

  1. Acquire idempotency lock on charge.id in Redis (5-min TTL)
  2. Load subscription, plan, customer, product, processor connection
  3. Inventory check via BC API (abandon if backorder policy forbids)
  4. Recalculate line-item pricing from current BC catalog (handle price changes since last renewal)
  5. Recalculate tax + shipping (call BC's checkout-calculation endpoint with subscription address)
  6. Dispatch to processor adapter (BC Payments / Stripe / …) with payment_method_ref, idempotency key = charge.id
    • BC Payments adapter: preferred path is BC Payments Processing API with a MIT-flagged Payment Access Token against the order; fallback is direct Braintree charge with the Braintree token
    • Stripe adapter: PaymentIntent with off_session: true, confirm: true, customer + payment_method
  7. On success: create BC order via V2 Orders API with payment_status: captured, processor transaction ID, tag with custom fields
  8. Update charges.status=succeeded, charges.bc_order_id=...
  9. Schedule next charge (advance anchor_date by interval)
  10. Emit charge.succeeded event + send renewal email
  11. On failure: branch to dunning state machine (see below). Note: for BC Payments renewals, PayPal's network decline signals (soft vs hard) feed directly into retry eligibility — we do not retry hard declines.

Dunning state machine:

  • Configurable per store (default: 5-stage retry at +12h, +12h, +24h, +48h, +72h (~6.5 days total), then cancel)
  • Each retry re-enters the executor with the same charge.id (new retry_attempt)
  • Subscription transitions: active → past_due on first failure, past_due → cancelled|paused on exhaustion per policy
  • Dunning emails sent at each failure with payment-update portal link
  • Subscriber-initiated payment-method update resets attempt counter and enqueues immediate retry

Reconciliation sweep (daily cron):

  • Detect: charges stuck processing > 1h → inspect at processor, resolve
  • Detect: BC orders tagged as subscription-origin but missing corresponding charge.bc_order_id → backfill
  • Detect: processor succeeded but BC order creation failed → manual queue

9.4 Recurring Order Generation

<!-- traceability:start:PRD:9.4 -->

Prototype: Detail

<!-- traceability:end:PRD:9.4 -->

Every successful charge produces a native BC order via V2 /orders POST, with:

  • Customer, billing/shipping from subscription snapshot (or override if customer updated)
  • Line items from subscription (quantity, variant)
  • payment_method: manual, payment_status: captured, payment_provider_id: <processor txn id>
  • custom_fields: subscription_id, charge_id, cycle_number, plan_id
  • staff_notes: [SUB] {subscription_id} cycle {N}
  • status_id: mapped to merchant's "Awaiting Fulfillment" (configurable)

Merchant's existing OMS/WMS/3PL sees these as regular orders. No parallel fulfillment ops.

9.5 Customer Portal (Subscriber Self-Service)

Routes (hosted; also available as headless SDK):

  • /portal/login — BC customer email + OTP (magic link) or BC storefront SSO
  • /portal/subscriptions — list
  • /portal/subscriptions/[id] — detail
  • /portal/subscriptions/[id]/payment-method — update
  • /portal/subscriptions/[id]/address — update shipping
  • /portal/subscriptions/[id]/orders — order history
  • /portal/subscriptions/[id]/skip-next
  • /portal/subscriptions/[id]/swap — change variant
  • /portal/subscriptions/[id]/pause — pause with date picker
  • /portal/subscriptions/[id]/reschedule — change next-charge date
  • /portal/subscriptions/[id]/cancel — triggers churn-prevention flow (9.7)

MVP operations list: skip, swap, pause, reschedule, cancel, update payment, update address, update quantity.

9.6 Merchant Admin

Dashboard (/stores/[storeHash]/dashboard):

  • MRR, active subs, churn rate (30d), new subs (30d), past_due count
  • Upcoming charges (next 7 days) — value + count
  • Exception queue count (failed payments, out-of-stock, reconciliation drift)
  • Recent events timeline

Subscriptions list:

  • Filter: status, plan, customer group, failed payment state
  • Sort: next charge date, MRR contribution, lifetime value
  • Bulk: skip next charge, pause, export

Subscription detail:

  • Current state, schedule, payment method health
  • Charge history (succeeded/failed, links to BC orders)
  • Event timeline (merchant, subscriber, system actions)
  • Manual actions: refund charge, skip, pause, cancel, re-schedule, update PM link

Plans:

  • Plan list (per product)
  • Create / edit / archive
  • Plan performance: active subs, avg cycles completed, churn

Exception queue:

  • Failed renewals in dunning → one-click "contact subscriber" / "extend grace"
  • Out-of-stock renewals → "use backorder" / "skip cycle" / "substitute SKU"
  • Reconciliation drift → "acknowledge" / "open in BC"

Settings:

  • Dunning policy editor
  • Notification template editor (Resend)
  • Processor connection — auto-detects BC Payments if enabled on the store; Stripe Connect onboarding link for Stripe path; only one primary processor at a time in MVP
  • Webhook health

9.7 Churn Prevention

Cancel flow is not a single "are you sure?" — it is a decision tree:

  1. Reason capture (radio): too expensive / don't need right now / product issue / ordering too much / other
  2. Matched intervention:
    • Too expensive → offer discount code (merchant-configured pool)
    • Don't need right now → pause 30/60/90 days
    • Ordering too much → offer longer interval
    • Product issue → escalate to support (Zendesk/Gorgias webhook)
    • Other → continue
  3. Final confirm
  4. Cancel → emit sub.cancelled with reason; write to events for retention analytics

Merchant can configure offers per reason. Phase 2: A/B test intervention efficacy.

9.8 Notifications

Transactional email (Resend) and optional SMS (Twilio). The seven subscriber-facing lifecycle email types:

  • Welcome (first charge succeeded → subscription.activated)
  • Upcoming charge (T-3 days, configurable → subscription.upcoming_charge)
  • Charge succeeded (→ subscription.renewed)
  • Charge failed (with update-payment CTA → charge.failed)
  • Dunning retry notice (per stage → charge.failed_permanently)
  • Subscription paused/resumed/cancelled (→ subscription.paused / subscription.resumed / subscription.cancelled)
  • Shipment generated (piggyback on BC's shipment webhook → subscription.shipment_generated)

Templates are merchant-editable (MJML or markdown) with standard variables.

Event-pipeline pattern. Producers publish logical lifecycle events via logEvent (ADR-0010 transactional-outbox). The apps/email-consumer Worker subscribes to subs-events, maps event type → template_key at consumption time, fetches the merchant-edited template (falling back to seeded defaults), and dispatches via Resend. Ratified by ADR-0063. The pipeline is not imperative "send this template" — producers do not name templates; they publish facts.

Phase-1 ship status. Renewal-confirmation (subscription.renewedrenewal_confirmation) is shipped end-to-end. The remaining six lifecycle types' consumer mapping + producer wiring are tracked at Hive #1505 (welcome, upcoming-charge T-3 scanner, charge-failed CTA, dunning retry, paused/resumed/cancelled portal events, shipment-generated webhook). Until that Spec lands, those events fire to the events table and queue but are skipped consumer-side.

Custom-domain strategy. Per-merchant DKIM keys via merchant-side DNS verification on notifications.{merchant.com} (primary). subscriptions.bigcommerce.com is the fallback for merchants who skip DNS — measurable activation drag for mid-market (see §13).

Transactional vs marketing routing rules. Every template tagged transactional | marketing. Transactional bypasses subscriber marketing opt-out (legitimate-interest basis under GDPR; CAN-SPAM transactional-message exception). Marketing emits the RFC 8058 one-click List-Unsubscribe-Post: List-Unsubscribe=One-Click header; transactional does not.

PCI variable allowlist. Templating layer enforces — not template-author trust. Allowed payment fields: payment_method.last4, brand, exp_month, exp_year. Blocked: full_pan, cvv, anything matching a card-number regex.

Magic-link security model.

  • Token: opaque 32-byte random; bcrypted lookup server-side; never PII in URL
  • TTL: 15 min (portal login) / 24h–7d (dunning payment-update, configurable per merchant)
  • Placement: token after # fragment (avoids Referer leakage)
  • Single-use: marked consumed atomically on first redemption; subsequent → "request fresh link"
  • Phishing resistance: domain matches the storefront the subscriber was last active on; reject mismatch

Idempotency contract. Three independent layers, all required:

  1. Per-send key {template_id}:{recipient}:{source_event_id} survives workflow restart
  2. Time-window dedupe (default 5 min, same (template, recipient)) survives event-storm cascades; off for magic-link / password-reset
  3. Resend Idempotency-Key header survives our retry to Resend

9.8.1 Email security & compliance

Authentication. Every send authenticates via SPF + DKIM + DMARC. Gmail/Yahoo enforce DMARC alignment for ≥ 5K/day senders since Feb 2024 — non-aligned sends route to spam at scale. DKIM signed via merchant-domain key (US-23.9); SPF includes Resend's mail servers; DMARC policy at minimum p=none with reporting, escalating to p=quarantine once aligned.

RFC 8058 one-click unsubscribe. Marketing-flagged sends include List-Unsubscribe: <mailto:...>, <https:...> and List-Unsubscribe-Post: List-Unsubscribe=One-Click. Transactional sends do not (legitimate-interest exemption).

CAN-SPAM (US). Physical merchant address in footer; functioning unsubscribe (marketing); accurate From and Subject (no deceptive routing). Apply to merchant-flagged-marketing sends; transactional exempt from unsubscribe but must still carry merchant identification.

CASL (Canada). Implied consent acceptable for transactional only. Marketing requires explicit opt-in proof retained 36 months.

GDPR (EU). Transactional emails sent under legitimate interest basis; data minimization in templates (only required fields rendered); PII retention rules per Epic 28.

PCI DSS. Variable allowlist enforced at the templating engine, not by reviewer trust. Templates that reference blocked fields fail render and emit alert (US-23.14).

PSD2 SCA. Payment-update magic-links during dunning are security artifacts: short TTL, single-use, opaque token, fragment-only URL placement.


10. Integration Surfaces

10.1 Inbound webhooks — from BC

  • store/order/created — fetch subscription intent from cart-level metafields (GET /v3/carts/{cartId}/metafields?namespace=bc-subscriptions&key=subscription_intents), create Subscription + Charge
  • store/order/updated — sync order status to subscription view
  • store/order/refund/created — reconcile to charge.refunded (planned; not yet registered)
  • store/customer/updated — update subscription payment address if customer updated default
  • store/customer/deleted — cancel all customer's subscriptions
  • store/product/updated — detect price changes, flag affected plans
  • store/product/deleted — pause affected subscriptions
  • store/app/uninstalled — purge credentials, pause all subscriptions

10.2 Inbound webhooks — from processor

BC Payments path (events consumed via BC webhook subscriptions where exposed; Braintree webhooks as fallback):

  • Transaction settled / declined / disputed
  • Payment method invalidated / expired
  • Dispute opened — auto-pause subscription, alert merchant
  • Subscription MIT authorization revoked (if exposed) — flag PM, block further renewals

Stripe path:

  • payment_intent.succeeded / payment_intent.payment_failed — authoritative charge outcome
  • payment_method.detached / payment_method.updated — flag PM as invalid, trigger subscriber notification
  • charge.dispute.created — auto-pause subscription, alert merchant
  • customer.updated — detect default PM changes

Adapter contract: each processor adapter normalizes its webhook stream into a small shared event vocabulary (charge.settled, charge.declined, pm.invalidated, dispute.opened) before it hits our Event table. The billing engine only consumes the normalized vocabulary.

10.3 Outbound (our webhooks + events)

  • REST API for merchants: /api/v1/subscriptions, /api/v1/charges, /api/v1/events
  • Webhook subscriptions for merchants: event types above
  • Event stream via Vercel Queues for internal consumers

10.4 Integrations (Phase 2+)

  • Klaviyo / Ortto (subscription lifecycle events)
  • Gorgias / Zendesk (cancel-reason escalations)
  • Avalara / TaxJar (tax on renewal orders — via BC's native tax, no direct integration needed)
  • Shipstation (inherits from BC orders, no direct work)

11. Analytics & KPIs

11.1 Merchant-facing metrics

  • MRR, ARR, net new MRR, MRR churn, gross churn, net revenue churn
  • Active subscriptions, new subscriptions, cancelled subscriptions (period-over-period)
  • Avg revenue per subscriber, avg cycles completed
  • Failed-renewal recovery rate
  • Portal self-service resolution rate
  • Cancel-reason distribution + intervention save rate

11.2 Platform-facing health metrics

  • Charge success rate (p50, p95, per-processor)
  • Dunning recovery curve
  • Time from charge → BC order created
  • Webhook processing lag
  • Reconciliation drift detection rate

12. MVP Cut & Phased Roadmap

Phasing model superseded by ADR-0021 — delivery scope is now governed by the Tier α/β/γ/δ cohort model, not the Phase 1/2/3 model below. The scope tables below remain accurate as a content enumeration of what ships in each cohort; the sequencing labels (Phase 1 / Phase 2 / Phase 3) map to Tier α / Tier β / Tier γ+δ respectively. Treat ADR-0021 as authoritative when the two diverge.

Phase 1 — MVP (target: 14 weeks; +2 weeks vs. v0.1 to absorb BC Payments adapter)

Ship: BC Payments + Stripe as processor adapters, core loop works end-to-end.

Includes:

  • OAuth install, App Extensions (Products, Orders, Customers), webhook registration
  • Plan creation (interval + fixed discount % or Price List link)
  • Single-product subscriptions (no bundles, no build-a-box)
  • Stencil storefront widget + headless SDK
  • Cart-metafield subscription intent capture (namespace: bc-subscriptions, key: subscription_intents)
  • BC Payments adapter (primary) — Stored Instruments vaulting + MIT renewals (or Braintree fallback per §6.2)
  • Stripe adapter (alternate) — Stripe Connect onboarding, PaymentIntent off-session renewals
  • Billing engine with tunable dunning (default 5-stage, ~6.5 days), soft/hard decline awareness
  • BC order creation on successful charge (tagged with subscription + charge IDs)
  • Subscriber portal (skip, swap, pause, reschedule, cancel, update PM, update address)
  • Merchant dashboard (MRR, active count, upcoming, exceptions, BC Payments reconciliation link)
  • Transactional email via Resend
  • Cancel-reason capture + simple pause-instead intervention
  • Reconciliation daily sweep
  • Audit event log

Deliberately excluded from MVP:

  • Braintree standalone / Authorize.net (covered partially via BC Payments' Braintree backend, but no direct Braintree adapter)
  • Multi-storefront / channel-scoped subscriptions
  • Build-a-box / bundles
  • Prepaid / fixed-term subscriptions
  • B2B Edition support
  • SMS notifications
  • Cancel-flow A/B testing
  • Customer-group–scoped plans
  • Migration from existing subscription apps (see Phase 2)

Phase 2 — Platform Depth (12 weeks post-MVP)

  • Braintree standalone + Authorize.net processor adapters
  • Multi-Storefront / channel-scoped plans
  • Customer-group–scoped plans
  • Prepaid / fixed-term subscriptions
  • SMS notifications
  • Klaviyo + Ortto integrations
  • Cancel-flow A/B testing + intervention analytics
  • Migration tooling (import from Recharge / PayWhirl / Bold — critical because we can offer these merchants a path to consolidate onto BC Payments)

Phase 3 — Enterprise (12 weeks)

  • B2B Edition support (company accounts, approval workflows, purchase orders as payment method)
  • Build-a-box / bundle subscriptions
  • Usage-based billing primitive
  • Advanced segmentation (RFM-based retention campaigns)
  • White-label portal (custom domain, full theming)
  • International BC Payments regions as BC rolls them out

13. Open Questions & Risks

Area Question / Risk Mitigation
BC Payments MIT surface (validated; PI-5062 reframed per ADR-0037) RESOLVED by Day-0 spike eb94cf4a (decision 256591ec): BC's BigPay layer auto-classifies API-initiated stored-instrument charges as MIT (MREC for recurring), production-confirmed at Wren Laboratories. ADR-0037 (2026-05-15) reframes PI-5062: it is a Worldpay/Paymetric-specific NTI threading gap, not a blocker for the standard stored-instruments vault rail. Standard merchants (BC Payments/PPCP, Braintree, Auth.Net) are unaffected. Adapter persists NTI from CIT response for Worldpay/Paymetric merchants until PI-5062 closes (PRD-COMPANION D17). EU PSD2 SCA exemption preserved across the chain.
BC Payments availability (new) BC Payments is US-only at launch (March 2026). International merchants cannot use Path A. Stripe remains fully supported as alt path; international MVP works on Stripe only. Track BC Payments geo rollout and add regions as they land.
BC Payments migration pacing (new) If PPCP → BC Payments migration is slower than BC projects, our TAM for Path A in year 1 is smaller than hoped Ship both Path A and Path B at MVP. Don't bet on BC Payments alone.
Stripe switching cost What % of target merchants currently on Stripe will accept us as a Stripe-only adapter vs. wait for BC Payments? Both adapters ship at MVP — no forcing function
Cart intent capture Will Stencil's line-item custom field API hold up for all theme variants? Build post-purchase fallback from day one; test against top 20 Stencil themes
Checkout divergence Merchants using third-party checkouts (Bolt, Fast, etc.) lose cart-custom-field capture Document as unsupported for MVP; add case-by-case in Phase 2
Price drift If product price changes between subscription creation and renewal, which price wins? Default: current catalog price. Merchant-configurable: lock at subscription creation.
Inventory conflicts What if a renewal runs when the product is out of stock? Configurable: backorder / skip cycle / substitute / pause subscription
Tax jurisdiction changes Subscriber moves mid-subscription; address change triggers tax re-calc — is that always right? Yes, always recalc at renewal time from current address
GDPR / PII Subscription history + address snapshots survive in our DB after BC customer deletion Hard-delete on store/customer/deleted webhook; retain only anonymized revenue records
BC API rate limits Large stores renewing 1000s of subs in the same hour could exceed 450 req/30s limit Queue-based rate limiting; spread renewals across the day using anchor_date jitter
Multi-currency BC supports multi-currency at the storefront level, but subscription prices are stored once Plan price denominated in store default currency; convert to presentment currency at display time only
Uninstall lifecycle What happens to active subscriptions if merchant uninstalls the app? Pause all subs, retain data 30 days, purge or retain-on-merchant-request thereafter
Custom-domain DNS adoption rate (proposed) Merchants who skip DNS verification get the weaker subscriptions.bigcommerce.com fallback — measurable activation drag for mid-market Track per-merchant; nudge in onboarding flow; consider tier-gated requirement at Enterprise plan. See Hive proposal e3ff617b.
Resend rate-limit fairness across merchants (proposed) Workspace-level rate limits shared across all merchants; one merchant's burst could starve others Per-merchant fair-share token bucket; reserve 10% of capacity for transactional regardless of marketing pressure. See Hive proposal e3ff617b.

14. Success Criteria (MVP exit)

Engine-works metrics — pipeline reliability:

  • 10 paying merchants onboarded
  • 1,000 active subscriptions across the platform
  • 95%+ charge success rate (p50), 90%+ (p95)
  • 85%+ dunning recovery rate within 7 days
  • <1% reconciliation drift (charges with inconsistent state between us ↔ BC ↔ processor)
  • Median subscriber self-service resolution: no merchant support ticket required for skip/swap/pause/PM-update
  • Median merchant exception-queue time to resolution: <1 business day

Differentiator metrics — wedge validation:

  • Path A mix [target]: ≥60% of MVP merchants on BC Payments adapter. Validates the BC-Payments-unification thesis. <60% means the pitch isn't landing or the path isn't smooth enough.
  • Onboarding step count: ≥30% fewer steps end-to-end (install → first subscription enabled) vs Recharge baseline. Empirical, not anecdotal — requires a one-off competitive audit before MVP launch to set the baseline. Validates the "lowest-friction install" claim from §3.
  • PM-update friction: ≤2 clicks to update payment method across N subscriptions for a customer with N>1. Validates D5 (PRD-COMPANION D17 / customer-level payment_customer_ref) — the cross-sub PM update UX that differentiates against Recharge's per-sub flow.

These are exit criteria, measured retrospectively to declare MVP successful — not blockers for shipping.


15. Appendix: Why Not Just Fork an Existing Solution

  • Recharge: closed-source, Shopify-first internals, licensing prohibits. BC Payments is not a supported gateway on any Recharge BigCommerce integration path (Recharge payment processors); adding it would require Recharge to re-architect against BC-specific payment rails — no public indication they plan to.
  • Stripe Billing: catalog-agnostic, would require building the BC integration surface separately anyway. Does not integrate with BC Payments.
  • Bold Subscriptions / PayWhirl / Rebillia: SMB-targeted, none currently integrate with BC Payments. Mid-market observability and B2B coverage gaps documented in §2.2.
  • Open source (e.g., Kill Bill): billing-engine-only, still need BC glue, data model, merchant UX, and the BC Payments adapter work.

The value is in the BC-native depth and the BC Payments unification, not the billing primitives.


16. Architectural Alternatives Considered

Rigor requires writing down the paths we did not take and why. Two in particular were re-examined after v0.2 feedback.

16.1 Alternative: Build on top of Stripe Billing instead of a native engine

What this would mean. Stripe Billing is not merely a payment vault — it is a full subscription engine: Product, Price, Customer, Subscription, Invoice, Quote, plus Smart Retries, Automatic Card Updater, Tax, Revenue Recognition, and a hosted Customer Portal that does skip/swap/pause/cancel self-service out of the box. Stripe claims 25M+ renewals per day on this infrastructure.

The alternative architecture would be: for Stripe-path merchants, we thin-shell Stripe Billing — mirror BC products into Stripe Products/Prices, create Stripe Subscriptions on checkout, let Stripe own scheduling/dunning/retries/portal, and consume Stripe webhooks to create matching BC orders after each invoice.paid.

What we would gain.

  • ~40% less engine code to build (scheduling, dunning, retries, proration, portal UI for Stripe path)
  • Inherited scale, reliability, and recovery tooling (Smart Retries alone recovered $6.5B across Stripe customers in 2024 per Stripe)
  • Tax + Revenue Recognition for free on the Stripe path
  • Automatic Card Updater network coverage for free
  • Stripe-hosted Customer Portal removes one UI surface for MVP

What we would lose — the case against.

  1. Catalog schizophrenia. BC is the source of truth for products, variants, and pricing. Stripe Billing requires a parallel Product/Price catalog. Every BC product edit triggers a sync job; every price change triggers a Stripe Price migration (Stripe prices are immutable). This is a perpetual reconciliation tax and a constant source of merchant support tickets.

  2. Order schizophrenia. Stripe Billing generates Invoices; BigCommerce generates Orders. Either we reconcile both (merchant sees two artifacts for one transaction) or we suppress Stripe invoices and lose half the Stripe Billing value. Our core positioning is "Orders are BC orders" — Stripe invoices are an alien artifact here.

  3. Split experience. BC Payments merchants get our native portal + dashboard; Stripe merchants get a Stripe-hosted portal + our dashboard. Two UX codepaths, two support runbooks, two feature roadmaps. Anything we can't expose through Stripe's portal (BC-specific: switch variant to a different BC product, add a BC coupon, apply a BC Price List) is unavailable to Stripe merchants — or requires us to rebuild it anyway.

  4. Price List unusable. BC Price Lists are a primary subscription-pricing mechanism in our design. They have no representation in Stripe Billing. On the Stripe path we would lose this entire differentiator, or have to materialize Price List outputs into static Stripe Prices and re-sync on every change.

  5. Customer Group / Channel unusable. Same problem. Stripe does not model BC Channels or Customer Groups. Plans scoped by either cannot exist on the Stripe Billing path.

  6. Lock-in asymmetry. A merchant leaving Stripe (for BC Payments, say) would need to re-create every subscription from scratch because Stripe Billing state cannot be exported and re-instantiated on BC Payments. Our native engine owns the source of truth and makes processor migration a PM-token swap.

  7. Two billing engines to operate anyway. BC Payments path must own scheduling, dunning, and portal logic — Stripe Billing cannot help there. So we end up operating two subscription engines (ours + Stripe's) with different semantics, observability, and failure modes, instead of one.

Verdict: Rejected as default architecture. Re-considered as a future merchant option.

We own the billing engine. Both BC Payments and Stripe adapters call a shared scheduling + dunning + portal layer. The Stripe adapter uses Stripe PaymentIntents + Customers + PaymentMethods + SetupIntents — not Stripe Subscriptions.

One narrow re-entry for Stripe Billing: in Phase 3, offer an "enterprise Stripe mode" for merchants who specifically want Stripe's Tax, Revenue Recognition, and Automatic Card Updater coverage and are willing to accept split catalog. This is opt-in, clearly labeled, and does not pollute the BC-native merchant experience.

16.2 Alternative: Support every major BC gateway at MVP

What this would mean. Ship Stripe + Braintree + Adyen + Cybersource + Worldpay + Authorize.net + others at MVP so any BC merchant with a recurring-capable gateway can install.

Why rejected.

  • Each adapter is 2–4 engineer-weeks minimum (vault API + charge API + webhook normalization + idempotency + reconciliation + sandbox testing)
  • Adapter surface area dwarfs the rest of MVP — we would ship 6 gateway adapters and no product
  • The core of our target mid-market BC segment runs on BC Payments (new/migrating merchants) or Stripe (existing mid-market); BC's other gateways are a long tail — the additional adapters unlock that tail, not the core TAM (segment-share magnitude is an internal [assumption], not sourced market data)
  • Adapter work is low-risk and parallelizable — Phase 2 can ship them rapidly once the adapter interface is proven with two production implementations

Verdict: Rejected for MVP. Phase 2 ships Braintree-standalone + Authorize.net + Adyen as the next three, driven by the gateway matrix in §6.3.

16.3 Alternative: Integrate with an existing OSS billing engine (Kill Bill, Lago, Metronome-OSS)

What this would mean. Mount an open-source billing engine behind our API and delegate subscription state, scheduling, and dunning to it.

Why rejected.

  • These engines are SaaS-billing-shaped (invoice-first, usage-metering-first) — not commerce-subscription-shaped (order-generation-first, inventory-aware)
  • Operational burden: running a billing engine as a sidecar adds a stateful service, a separate DB, and its own operational runbook
  • We still need to write every BC integration (catalog, orders, customers, payments) on top of it
  • The engine we need is small (a scheduler, a state machine, a dunning policy runner) — forking 3% of Kill Bill's surface is not a win

Verdict: Rejected. Our native engine is scoped to commerce-subscription needs and stays small.

16.4 Alternative: White-label an existing BC subscription app (MINIBC, PayWhirl, Rebillia)

Why rejected. Closed-source, no licensing precedent, and if any of them were the right answer the market opportunity would not exist.