Skip to content

2. Decision register

Generated from a canonical source

This page is a read-only projection of docs/solution-review/02-decision-register.md. Edit the canonical file, then run npm --prefix tools/project-knowledge-derive run derive.

This register distills the decisions in bc-subscriptions' 93-ADR (architecture decision record) corpus that bear directly on how the product sits on the BigCommerce platform — roughly two dozen ADRs, grouped into five themes. A review room will not read 93 ADRs; these are the decisions whose outcome a BigCommerce product, architecture, or platform lead would want to interrogate.

Read each entry as a one-screener. Decision is the ruling in one sentence. Why carries the constraint or failure mode that forced it — the causal driver, not an assertion. Alternatives rejected names what was considered and why it lost. Status & revisit trigger says whether it still holds and what condition would reopen it.

Every entry ends with a link to its full ADR for provenance — a relative path such as ../decisions/0037-stored-instruments-as-canonical-charge-rail.md. You do not need to follow it: the substance is here, and a reviewer without repository access can treat this file as self-contained. Where two or three ADRs decide one thing together, they share a screener titled by the primary, with companions named inline. A closing index table lists every entry for scanning or projection in the room.

2.1 Delivery model & runtime

How the product is positioned on the platform, what language and database run it, and where it runs next.

ADR-0029 — Marketplace-first, native-ready (with ADR-0022, marketplace merchant-of-record scope)

Decision. Ship bc-subscriptions as a BigCommerce App Marketplace product for the foreseeable horizon, applying a bounded set of BigEng-aligned (BigCommerce internal-engineering) design conventions so that if a future "native" in-platform graduation ever opens, the lift is months not years; the companion ADR-0022 defers marketplace merchant-of-record (MoR — the party that legally settles the funds) out of Phase 1 while pre-shaping the multi-actor data hooks.

Why. A 2,665-page crawl of BC's internal docs found zero in-flight shopper-subscription roadmap and explicit positioning (PROJECT-1986) treating well-built partner apps as a destination, not scaffolding — so designing as if native graduation were imminent buys design tax without payoff, because graduation is a BC-side org decision the partner does not control. ADR-0022 defers MoR because no marketplace deal is funded (building ~20K per-clinic processor connections speculatively risks 4–6 weeks of MVP — minimum viable product — slip) and the hard primitive is multi-actor modeling, which stays an additive migration later rather than a shape change now.

Alternatives rejected. "Waypoint" framing (native is imminent) — justified design tax against an event the partner cannot control. Building marketplace-MoR primitives in Phase 1 (ADR-0022) — speculative slip for an unclosed deal; Stripe Connect vs Adyen MarketPay deliberately left to v2.

Status & revisit trigger. Both accepted. ADR-0022 reopens when a marketplace deal lands (fires the documented v2 processor-connections migration); the native-readiness posture is policed each ADR cycle against "did we pre-build a primitive with no marketplace use."

Full record: ADR-0029 (companion ADR-0022).

ADR-0030 — The BigEng convention catalog (what "native-ready" concretely means)

Decision. Adopt the 12 artifact-level conventions BigCommerce's internal services share — snake_case wire fields, store_hash in every payload, cursor pagination, typed domain errors, an AuthPrincipal envelope, UUID identity, soft-delete, an operations ledger, and audit-decorator wrapping among them — as canonical for the codebase, with a subset enforced by CI (continuous-integration) gates on every pull request.

Why. ADR-0029's native-ready leg is only a real hedge if it is concrete; left in an audit doc the conventions drift between implemented and ratified. A 2026-05-12 audit found 2 of 12 already compliant and drove a full retrofit so a future graduation conversation has a bounded lift — and each convention carries standalone marketplace value regardless of graduation (typed errors improve partner developer experience; the audit decorator improves compliance posture).

Alternatives rejected. Phased retrofit across several cycles — rejected in favor of one full-retrofit cycle. Leaving the catalog unratified in the audit doc — leaves a standing implemented-vs-ratified gap.

Status & revisit trigger. Accepted; largely shipped, with a few conventions deferred by design (e.g., TIMESTAMP auto-update semantics wait for the Phase 2 Postgres move, since SQLite cannot express them). Reopen if a 13th convention surfaces in a future BigEng audit.

Full record: ADR-0030.

ADR-0054 — PostgreSQL 15 for the Phase 2 GCP database

Decision. When bc-subscriptions migrates from Cloudflare (Phase 1) to Google Cloud (Phase 2), the Cloud SQL engine is PostgreSQL 15 — not MySQL.

Why. Two internal memory records contradicted each other (Postgres vs MySQL). The tie broke to Postgres because the authoritative synthesis said so, a maintained 28-file Postgres migration tree already exists (a MySQL choice would strand that sunk effort), and the schema relies on ~8 partial (filtered) indexes that Cloud SQL MySQL 8.0 cannot express natively.

Alternatives rejected. Cloud SQL MySQL 8.0 — matches the reference Terraform layout, but strands the existing Postgres tree, regresses partial indexes, and its only claim to authority was incidental copied-from-reference-repo text.

Status & revisit trigger. Accepted. No revisit trigger recorded (Postgres 16 is noted as a later stable-track upgrade, not a reopen).

Full record: ADR-0054.

ADR-0091 — TypeScript is the backend language

Decision. Keep TypeScript (on Cloudflare Workers today) as the backend language, and re-choose it on a from-scratch basis — conditional on retaining an edge/serverless runtime.

Why. BC's internal standard forbids Node/TypeScript for native services on stated "performance" grounds, but this workload is I/O-bound orchestration (calling BC and BigPay — BigCommerce's payment service — APIs, database reads, webhook fan-out, cron-driven charge scheduling) where the language runtime is not the bottleneck; TypeScript also gives one shared wire contract across all three TypeScript frontends and is the most reliably agent-authorable language for a largely agent-written codebase.

Alternatives rejected. Rust — rejected as a whole-backend choice; warranted only for an isolated high-throughput hot path this system does not have. Kotlin — held as a conditional future contender that forfeits the edge model and the shared-types win.

Status & revisit trigger. Accepted. Reopens only if a full rewrite also commits to long-running containers AND a CPU/latency-critical charge-processing path at scale is expected.

Full record: ADR-0091.

2.2 Money & payments

How recurring charges actually move — the rail, its sequencing and capture timing, retry semantics, tax, and what stays out of PCI scope.

ADR-0037 — Stored-instruments vault is the canonical charge rail

Decision. Every recurring subscription charge routes through BigCommerce's stored-instruments vault (payments.bigcommerce.com/stores/{hash}/payments) using the vault token captured at checkout, regardless of which BC-supported gateway the merchant runs.

Why. "BC Payments" is not a distinct processor requiring its own tokenization code — it wraps PayPal's commerce platform, and every BC gateway with "Stored Payments" enabled participates in the same gateway-agnostic vault surface. Routing all charges through it means raw card data never touches the app and no per-gateway client SDKs (Stripe Elements, Braintree.js) are needed; otherwise the codebase would carry misleading per-gateway complexity for the standard case.

Alternatives rejected. Per-gateway adapters as parallel first-class rails — architecturally correct for edge cases but misleading complexity for the standard case; the Stripe-direct adapter is retained only as the non-vault edge case.

Status & revisit trigger. Accepted; live-validated end-to-end for the Stripe gateway per ADR-0082 (the PayPal-powered gateway's same-session vault confirmation remains open). No revisit trigger recorded.

Full record: ADR-0037.

ADR-0035 — BC Payments standard-rail endpoint contract

Decision. Use BigCommerce's standard, GA (generally available) Payments API for merchant-initiated recurring charges, via a verified three-call sequence: mint a Payment Access Token (PAT) with is_recurring: true, discover the stored instrument's payment method for the order, then post the stored-card charge.

Why. The production BC Payments branch had been throwing because it was gated on blockers — PI-5062 (a BigCommerce platform ticket about Worldpay network-transaction-ID threading) and partner-track beta enrollment — later found to apply only to a beta rail; the payments.bigcommerce.com endpoint is in fact the standard GA surface available to all merchants, with each call verified against BC's published REST docs.

Alternatives rejected. A dedicated /v3/payments/recurring endpoint — does not exist; the is_recurring flag on the PAT mint is the mechanism. Direct Braintree proxy — no documented merchant path. BigPay's internal checkout endpoint — not exposed to external API consumers.

Status & revisit trigger. Accepted. No revisit trigger recorded.

Full record: ADR-0035.

ADR-0025 — Charge sequencing per processor (order-first vs charge-first)

Decision. Charge sequencing is a per-adapter concern flagged by a requiresPreExistingOrder boolean: the vault / BC Payments rail creates the BigCommerce order first and then charges against it; Stripe-style adapters charge first and materialize the order afterward. The scheduler owns order creation so the BC coupling stays in one place.

Why. The vault charge call requires an existing order — GET /v3/payments/methods needs an order_id, and the Payment Access Token is minted for a specific order — so there is no documented "charge a stored card outside an order" path; the pre-existing scheduler was charge-first across all adapters and therefore could not ship the BC Payments rail at all.

Alternatives rejected. A two-phase prepareCharge/confirmCharge interface on every adapter — rejected as ceremony imposed on all adapters to serve the one adapter that needs the divergence today.

Status & revisit trigger. Accepted. Revisit if more order-first adapters appear (at which point the two-phase interface is reconsidered).

Full record: ADR-0025.

ADR-0038 — Charge capture timing

Decision. Phase 1 ships an advisory stores.capture_timing column (immediate / on_fulfillment / on_ship); the scheduler still charges atomically regardless of the value. Actual deferred capture — splitting authorize() from capture() — is a deferred Phase 2 build requiring its own [Spec].

Why. EU rules (PSD2, the EU payment-services directive, plus Visa/Mastercard scheme rules) require capture no earlier than shipment for physical goods, and authorizations expire in ~5–7 days, so physical-goods subscription boxes need deferred capture. But the authorize/capture split touches the scheduler, every adapter, and order-lifecycle webhooks, so the cheap config column ships now to record merchant intent and provide the Phase 2 data shape without a second migration.

Alternatives rejected. Bundling the full authorize/capture split into Phase 1 — would block the cheap, useful column behind a large-scope PR.

Status & revisit trigger. Phase 1 accepted (advisory only); the Phase 2 deferred-build clause is not satisfied until a [Spec] proposal is filed. EU physical-goods merchants remain non-compliant on capture timing until Phase 2.

Full record: ADR-0038.

ADR-0011 — Charge idempotency and retry semantics

Decision. The charge idempotency key is ${subscription_id}:${cycle_anchor_iso}, stable across every retry attempt (no per-attempt suffix), backed by a UNIQUE partial index; retries reuse the key, and the charge-status enum gains a failed_permanently terminal state for exhausted or hard-declined charges.

Why. The cycle anchor is the unique identity of a logical renewal and must survive retries, because the scheduler parses the anchor back out of the key to schedule the next cycle, gateways dedupe on the caller-supplied key, and the partial UNIQUE index is the SQL safety net against double-ticks — any per-attempt mutation of the key breaks all three at once.

Alternatives rejected. Per-attempt key rotation (a :attempt_n suffix) — breaks the cycle-anchor reparse and gateway-side dedupe. Typed-error detection of double-ticks instead of substring-matching the error message from D1 (Cloudflare's SQLite database) — D1's error surface exposes no type discriminator.

Status & revisit trigger. Accepted; several legs are Phase 2 (the retry-with-backoff curve, the SCA — strong customer authentication — requires_action wake path), some gated on PI-5062. No revisit trigger recorded (per-store retry-curve overrides are noted as a Phase 3 follow-up).

Full record: ADR-0011.

ADR-0060 — PCI stays in SAQ-A by construction

Decision. No component owned by bc-subscriptions receives, stores, transmits, or logs raw cardholder data — no card number, no CVV (card verification value), no full PAN (primary account number) — which keeps the app in PCI (payment card industry) SAQ-A, the lowest-burden self-assessment tier.

Why. SAQ-A applies only while raw card data never passes through the app's API, database, or logs; any breach of that invariant (a logged card field, a new integration that proxies card data) immediately escalates the app to SAQ-D — a full QSA (qualified security assessor) audit — which is disqualifying for marketplace timelines. Card entry lives inside Stripe Elements iframes and BC's hosted vault portal; the app persists only opaque tokens.

Alternatives rejected. None recorded in the ADR — it ratifies an existing architectural boundary rather than choosing among options.

Status & revisit trigger. Accepted. Reopens if any future integration requires server-side card handling (for example a custom adapter without hosted fields) — which would need explicit re-scoping and an SAQ-D-aware design review.

Full record: ADR-0060.

ADR-0016 — Tax is pass-through to the merchant's BC tax engine

Decision. The subscription app runs no tax engine; it defers to whatever engine the merchant already configured in BC admin (TaxJar / Avalara / Vertex / BC built-in). As built (US-15.3 / Epic 15), the renewal path quotes current-jurisdiction tax from BC's Checkout Consignments API and adds it to each cycle additively — charges.amount_cents is the pre-tax base, tax_cents carries the tax — recomputed live every cycle rather than from a snapshot. On a Consignments lookup failure it falls back to untaxed (tax_source='fallback_zero') rather than blocking the charge.

Why. Any capability BC already owns should default to BC — the merchant configured tax once in BC admin, and layering a second app-chosen engine on top is a category error that adds vendor lock-in, per-merchant secret liability, and tax-specific schema columns that would need migrating whenever a merchant switches engines. Live recompute also means a statutory rate change in BC admin propagates automatically into the next renewal.

Alternatives rejected. Direct TaxJar or Avalara integration — vendor lock-in, a parallel billing relationship, and schema cost. No-tax MVP — a non-starter the moment the app charges any merchant in a taxable jurisdiction, and no cheaper to ship than the pass-through.

Status & revisit trigger. Accepted. The as-built mechanism diverged from the ADR's original order-grand-total description to an additive Consignments model (US-15.3) — reconciled in ADR-0016's 2026-07-15 amendment. Open: whether the silent fall-back to zero tax on a Consignments failure is the intended posture or a compliance gap to close (candidate for the Epic 21 exception surface).

Full record: ADR-0016.

2.3 Data & tenancy

One database for all merchants, how tenant rows stay isolated, and where per-merchant credentials live.

ADR-0009 — Single database, store_hash isolation, enforced in the app layer

Decision. All merchants share one Cloudflare D1 (its serverless SQLite database); every non-global row carries a store_hash, and cross-tenant isolation is a mandatory application-layer scope guard — every read-by-id helper takes and checks store_hash — verified by a CI-blocking cross-tenant test suite.

Why. Single-D1-with-store_hash is marketplace-audit-defensible only if the app-layer controls are written down, code-enforced, and continuously verified; an audit found four helpers reading rows by primary id with no store_hash check, where the only thing preventing cross-tenant disclosure was a load-bearing accident. Per-store databases would add 30–60 seconds of provisioning to the OAuth install path.

Alternatives rejected. Per-store schema (logical databases) — same row ceiling, thin D1 tooling, closes no audit gap the scope guard doesn't. Per-store physical database — provisioning hostility on install and account-level database-count limits.

Status & revisit trigger. Accepted. Reopen if a marketplace privacy reviewer escalates past logical isolation to a stronger requirement (field-level encryption is the deliberately kept-open escape valve).

Full record: ADR-0009.

ADR-0004 — Encrypted OAuth token in stores.metadata JSON

Decision. The merchant's BigCommerce OAuth access token is stored AES-256-GCM-encrypted inside the existing stores.metadata JSON column (with a versioned v1: wire prefix), rather than in a dedicated schema column.

Why. The Phase-0 hackathon schema shipped with no column for the token, and the OAuth work needed somewhere to persist it without forcing a mid-Day-1 schema-change synthesis cycle; the JSON field unblocked the work, keeps the token encrypted at rest, and stays recoverable — a later migration can extract it to a real column with no data loss.

Alternatives rejected. Adding a stores.access_token_encrypted migration then and there — forces a schema-change cycle mid-build and blocks the OAuth roundtrip.

Status & revisit trigger. Accepted as a [Decision-Fast] hackathon shortcut; column extraction is explicitly reserved as a separate post-hackathon [Spec] (the JSON-blob shape is called out as unacceptable for production). No dated revisit trigger recorded.

Full record: ADR-0004.

ADR-0059 — Per-merchant Storefront API token, not an app-wide env var

Decision. The BigCommerce Storefront API token is per-merchant identity: each install mints and persists its own token (AES-256-GCM-encrypted on the stores row); the app-wide env.BC_STOREFRONT_TOKEN survives only as a sandbox-only transitional fallback.

Why. BC's token-mint endpoint is store-scoped by URL construction — a token minted from store A authenticates only against store A — so there is no such thing as a multi-merchant Storefront API token; the shared-env-var model works only in a single-tenant sandbox and breaks by construction the moment a second merchant installs. This is the same lesson as the per-merchant webhook signing-secret split (any state that varies per merchant must live on the stores row, not in app-wide config).

Alternatives rejected. None recorded as formal alternatives — the ADR frames the env-var model as structurally wrong rather than a competing option.

Status & revisit trigger. Accepted. Env-var retirement is telemetry-gated (a follow-up [Spec] once the env-fallback warning stops firing), not calendar-gated.

Full record: ADR-0059.

2.4 Events & integration

How state changes leave the system — the delivery substrate, its loss-recovery, and the read API for integrators.

ADR-0010 — Event egress on Workers Queues (with ADR-0027, the outbox catch-up cron)

Decision. Outbound events — internal fan-out and merchant-facing webhooks — run on Cloudflare Workers Queues with a transactional outbox at logEvent, HMAC-signed payloads (hash-based message authentication code), bounded exponential-backoff retry, a dead-letter queue (DLQ), and semver schema versioning; the companion ADR-0027 ships the events.queue_published_at outbox marker plus a per-minute catch-up cron.

Why. Phase 1 only wrote event rows to the database with no fan-out, but merchant webhooks (Epic 27) need at-least-once delivery, HMAC signing, replay protection, and a DLQ as marketplace table-stakes — shipping those with the substrate avoids breaking every merchant integration twice. ADR-0027 exists because without the outbox marker a transient queue-send failure was silent data loss: the row recorded intent, the queue never saw the message, and nothing drove a catch-up.

Alternatives rejected. Pure queue with no outbox — loses replay against the event table and breaks the dashboard read path. Direct fetch from each producer — no retry or DLQ, fails at-least-once. Durable Object (Cloudflare's stateful per-key primitive) per merchant — isolation not worth the operational complexity yet. (ADR-0027 also rejected a KV — key-value store — marker and a producer-side DLQ.)

Status & revisit trigger. Accepted; a 2026-06 amendment makes selective per-subscriber delivery the Day-1 contract rather than a v2 add-on. Revisit only if a strictly-better Cloudflare primitive emerges (its own follow-up ADR).

Full record: ADR-0010 (companion ADR-0027).

ADR-0074 — Read-only GraphQL surface on a hand-rolled engine

Decision. Expose POST /api/v1/graphql as a read-only GraphQL surface over Subscription → Charge → Order, backed by a ~250-line hand-rolled tokenizer + parser + executor rather than the standard graphql-js library.

Why. The requirement is a read-only projection over three related types — no mutations, variables, fragments, or streaming subscriptions — for which graphql-js's parse/validate/execute machinery is heavy on a Worker; hand-rolling also keeps the load-bearing tenant-isolation filter (every resolver scoped to the authenticated store_hash, per ADR-0009) under direct, auditable control.

Alternatives rejected. graphql-js, the canonical library — heavy bundle weight for a 3-type read surface and less direct control of the store_hash filter.

Status & revisit trigger. Accepted. Revisit triggers explicitly recorded: mutation support requested, variables/fragments needed, the type graph grows past ~6 types, or query-cost abuse observed — any of these means migrating to graphql-js.

Full record: ADR-0074.

2.5 Surfaces & auth

The three UI surfaces, the merchant-admin stack, the checkout/sign-in money path, and the merchant copilot.

ADR-0005 — Three UI surfaces (with ADR-0012, the merchant-admin stack)

Decision. bc-subscriptions ships three independently-deployed UI surfaces — an internal prototype, the merchant admin (loaded inside BC's control-panel iframe), and storefront components (composed into merchants' headless storefronts) — that communicate only by contract, never by shared bundle; the companion ADR-0012 locks the merchant-admin stack to Vite + React + BigDesign.

Why. The three audiences were never named as distinct surfaces, which repeatedly blocked production work on "where does this code actually ship." ADR-0012 picks Vite over Next.js because server-side rendering has zero value inside an auth-gated iframe, BigDesign hard-pins styled-components 5 (which does not compose cleanly with Next.js React Server Components — RSC — without marking every component client-side), and ~20 prototype slices port wholesale to Vite instead of being rewritten.

Alternatives rejected. A shared cross-surface bundle — rejected for contract-only boundaries. (ADR-0012:) Next.js App Router — SSR-no-value, Cloudflare-runtime adapter caveats, styled-components-5/RSC tension, and port-becomes-rewrite cost. Worker-served HTML — hackathon placeholder only.

Status & revisit trigger. Both accepted. ADR-0012 is the first rewrite candidate if BigCommerce ever mandates Next.js as a single app-development standard.

Full record: ADR-0005 (companion ADR-0012).

ADR-0090 — Subscriptions require sign-in; the storefront carries customer context

Decision. A cart containing a subscription gates on a signed-in customer (a guest is redirected to sign-in first), and the headless storefront must carry the customer access token into every cart operation and into a session-sync checkout handoff; one-time purchases stay guest-friendly.

Why. BigCommerce only vaults a stored instrument at checkout for a signed-in customer, so a guest subscribe produced an un-renewable subscription — nothing for the recurring rail to charge. Worse, the Svelte storefront created carts with the app storefront token, never the shopper's customer token, so even a logged-in shopper checked out as a guest (customer_id 0) and no headless subscription could ever vault.

Alternatives rejected. Keeping guest subscribe first-class with a portal add-card IAT (Instrument Access Token) on-ramp — the prior PRD §6.3 funnel premise — because it depends on the guest returning via a magic-link email before the first renewal; that is a recovery affordance, not a default subscribe path.

Status & revisit trigger. Accepted (2026-07-05); supersedes funnel decision 051b26d2 and amends ADR-0026 (below). No revisit trigger recorded.

Full record: ADR-0090.

ADR-0026 — Retire /api/checkout/intent; the cart-flow is canonical

Decision. Delete the legacy POST /api/checkout/intent endpoint and every client of it; the BigCommerce cart flow — Add-to-Cart carries a plan metafield, and the store/order/created webhook creates the subscription from the BC order — is the canonical subscribe-and-save path.

Why. A storefront direction change (the "north-star" pivot) rewired the product-detail widget to drive Add-to-Cart, leaving the intent endpoint dead and shape-incompatible (the route posted one body shape while the handler validated another, failing since the pivot); a live order confirmed the cart flow is server-authoritative with no client-trusted customer IDs, so the customer-derivation hardening concern simply does not exist on the live path.

Alternatives rejected. Harden the deprecated endpoint instead of retiring it — forecloses on accreting hardening work against a surface with no production path.

Status & revisit trigger. Accepted, but partially superseded by ADR-0090: ADR-0026 kept the plain ?cart_id= buildBcCheckoutUrl handoff, which ADR-0090 deletes and replaces with the customer-token session-sync handoff (createCartRedirectUrls). No independent revisit trigger recorded.

Full record: ADR-0026.

ADR-0085 — One shared portal-hosted checkout page for gift/prepaid money paths

Decision. Build the prepaid (US-6.2) and gift (US-6.1) purchase UI once, as a generic portal-hosted SvelteKit page parameterized entirely by URL query params, that both product-detail-page (PDP) widgets deep-link to — rather than duplicating a money-path form into each storefront platform.

Why. Duplicating a purchase form plus auth handling into each PDP widget (Stencil vanilla JavaScript, Catalyst React) multiplies a real money path across surfaces that cannot share code, and each copy re-derives the same two hard problems: anonymous-to-portal magic-link auth, and payment-method provisioning for a buyer who has no subscription yet (previously a production dead-end, since the only mint path required an existing subscription).

Alternatives rejected. Duplicate the purchase form and auth into each PDP widget — multiplies the money path across surfaces that cannot share code and re-derives auth and provisioning per copy.

Status & revisit trigger. Accepted. No revisit trigger recorded (a pre-existing cross-origin magic-link-return gap is flagged but explicitly not fixed here).

Full record: ADR-0085.

ADR-0083 — Merchant copilot (subs-assistant) on the codemode runtime

Decision. Build the merchant copilot (apps/assistant) on the "codemode" pattern — a per-store Durable Object where the model writes TypeScript against typed read-only stubs that execute in a network-isolated sandbox — instead of a classic serial AI tool-loop, adding WebSocket (WS) rate limiting and a per-turn cost ledger as first-class substrate.

Why. Compound merchant questions ("which subscriptions failed charges this week and what do they share?") chain 5–10 serial tool calls that each cost an LLM round-trip and degrade at step caps, whereas one codemode script chains arbitrarily many API calls with zero round-trips between them and cannot exfiltrate credentials even under adversarial prompting; the reference implementation (ask-bc) had two known gaps — no WS rate limit, no cost ledger — fixed here as substrate.

Alternatives rejected. A classic AI-SDK serial tool-loop — degrades at step caps on compound queries. A hand-rolled sandbox loader — unnecessary once the @cloudflare/think / @cloudflare/codemode libraries were verified installable.

Status & revisit trigger. Accepted; hardened across successive red-team amendments (stored XSS — cross-site scripting — via link href, link-based data exfiltration, fan-out denial-of-service, confirmation integrity). Reopen per feature for an admin resume route, customer-facing surfaces, or model-driven writes beyond pause/skip.

Full record: ADR-0083.

2.6 Quick index

ADR Theme One-line decision
0029 / 0022 Delivery model & runtime Marketplace-first product; native graduation is a hedge, not a commitment; marketplace merchant-of-record deferred to v2.
0030 Delivery model & runtime Adopt BigCommerce's 12 internal-service conventions as canonical — the concrete meaning of "native-ready."
0054 Delivery model & runtime Phase 2 Google Cloud database is PostgreSQL 15, not MySQL.
0091 Delivery model & runtime TypeScript stays the backend language, conditional on an edge/serverless runtime.
0037 Money & payments All recurring charges route through BC's gateway-agnostic stored-instruments vault.
0035 Money & payments Standard GA BC Payments API, three-call Payment-Access-Token sequence, for recurring charges.
0025 Money & payments Charge sequencing is per-adapter: vault rail order-first, Stripe-style charge-first.
0038 Money & payments Phase 1 ships an advisory capture-timing column; real deferred capture is Phase 2.
0011 Money & payments Idempotency key is subscription + cycle-anchor, stable across retries.
0060 Money & payments No raw card data anywhere; the app stays in PCI SAQ-A by construction.
0016 Money & payments No internal tax engine; pass through the merchant's BC tax config, recomputed live.
0009 Data & tenancy One shared D1 database; store_hash isolation enforced in the app layer plus a CI test.
0004 Data & tenancy OAuth token stored AES-256-GCM-encrypted in stores.metadata JSON.
0059 Data & tenancy Storefront API token is per-merchant, minted at install; the env var is sandbox-only.
0010 / 0027 Events & integration Event egress on Workers Queues with a transactional outbox marker + catch-up cron.
0074 Events & integration Read-only GraphQL on a hand-rolled minimal engine, store_hash-scoped.
0005 / 0012 Surfaces & auth Three contract-only UI surfaces; the merchant admin is Vite + React + BigDesign.
0090 Surfaces & auth Subscriptions require sign-in; the storefront carries customer context into checkout.
0026 Surfaces & auth Retire /api/checkout/intent; the BC cart-flow is canonical (partly superseded by 0090).
0085 Surfaces & auth One shared portal-hosted checkout page for gift and prepaid money paths.
0083 Surfaces & auth Merchant copilot on the codemode sandbox runtime, isolated per store.