Shopify Multi-Merchant OAuth Install — Design Note (P1 §C4)

Status: Design note v1.0 (2026-08-12) · P1 item C4, docs/roadmap/P1_BUILD_PLAN.md §C4 — "Design note first; this is the largest P1 piece and the only one that changes an auth boundary." Everything in this document is [design target] / [IN BUILD — not started] unless a line cites shipped code by file:line, in which case the label on that line governs. The platform ceiling is built, pre-benchmark (TRUTH.md); nothing here is a performance or security claim beyond what the cited code enforces today. Lane canon: docs/product/SIGNAL_OVERVIEW_v5.md → docs/product/SIGNAL_SHOPIFY_MASTER_v4.md (build spec; §2.1 ingestion, §3.5 compliance, §3.6 open owner decisions). Governing constraints: docs/roadmap/P1_BUILD_PLAN.md §A (measured inventory), §D (standing constraints: L0/L1 only, consent fail-closed, deliberate gateway landings); docs/roadmap/SIGNAL_SHOPIFY_PHASE_BINDING.md (P1 ceiling); .claude/memory/active/marketing-commerce-connectors.md ("Non-negotiable architecture", "Production gates"); docs/architecture/FLEET_INTEGRITY.md (tenant boundary); AGENTS.md 6.6 (every persisted/contract change is a migration, never a bare rename). External facts (scope names, token semantics, compliance-webhook rules) were verified against shopify.dev on 2026-08-12; each such fact is marked (doc-verified).


1. Measured current posture — the migration baseline

Measured against the tree on 2026-08-12 (rule 01-verification-discipline: the inventory is measured, not inherited). This section is the real posture the migration starts from; it matches P1_BUILD_PLAN.md §A with three refinements noted in §1.1.

Surface Measured state
Webhook ingress POST /webhooks/shopify (services/service-marketing-connectors/main.py:450-537), public, HMAC-first. _verify_shopify_hmac (main.py:341-348) computes base64(HMAC-SHA256(raw_body, secret)) against X-Shopify-Hmac-Sha256 — exactly Shopify's webhook signature shape (doc-verified) — keyed by the single env SHOPIFY_WEBHOOK_SECRET (main.py:57). Unset secret ⇒ every delivery 401 (main.py:341-342, 460-461) — the §A "blocked on operator secrets" state; zero live traffic has crossed the hardened gate.
Webhook tenant X-Mizoki-Tenant-Id header, else DEFAULT_TENANT_ID env (main.py:491-493). Shopify sends no such header, so in production the route is single-tenant-by-env. README.md:97 says "deploy one connector revision per tenant" for multi-tenant — this note supersedes that guidance (§5).
Hardening (PR #654, merge 59782a39) Replay cache post-HMAC (main.py:469-471, webhook_hardening.py:104-136); GDPR topics ACK+audit, never ingest (main.py:474-485, GDPR_TOPICS at webhook_hardening.py:266); inventory topics → Cell 37, 503 cell37_unconfigured when unset (main.py:487-489, 427-431); consent gate FAIL-CLOSED over unified.consent_registry_latest — registry disabled or erroring ⇒ redact (main.py:498-512; webhook_hardening.py:140-202, closed default at :164-166, 181-183); person-topic scope is an explicit market-safe allowlist, unknown topics fail closed (webhook_hardening.py:277-294). deployed, not live-verified.
Bulk backfill POST /api/v1/shopify/sync (main.py:400-419) → _pull_shopify (main.py:308-338) reads module-level env SHOPIFY_SHOP_DOMAIN / SHOPIFY_ADMIN_ACCESS_TOKEN (main.py:55-56); 503 when unset (main.py:231-235). One shop per deployment: the request's tenant_id selects where records land, not which store is pulled.
Caller auth verify_caller on every non-webhook route (contracts/mizoki_contracts/auth.py:85-120): Google-signed OIDC, audience + allowlist, fail-closed 503 on misconfiguration in a deployed environment (auth.py:72-82) — [enforced today].
Tenant enforcement Gap. resolve_tenant + the TENANT-001 registry (caller-SA → tenants; Firestore tenant_registry, contracts/mizoki_contracts/auth.py:123-157, contracts/mizoki_contracts/tenancy.py) exist in the shared contracts but are never invoked anywhere in this service (grep-verified 2026-08-12). tenant_id flows from request body / header / env default unchecked. The connectors-memory production gate "tenant registry enforcement" is therefore not yet met on this surface.
Per-tenant credential store Already exists, unwired to the data plane. connector_credentials.py (SecretManagerCredentialStore, :243-371) stores one JSON secret per (tenant, provider) as mizoki-connector-{tenant}-{provider} (:48, :289-293), labeled, versioned, masked-hints-only, never echoed; Shopify field spec = shop_domain + admin_access_token (:63-76). The sync/webhook paths do not read it.
Web Pixel path services/intent-shopify-extender — superseded 2026-08-12 (single-ingress collapse). It no longer receives Shopify webhooks and no longer holds the webhook secret: the gateway's /webhooks/shopify is the one boundary, and the extender is a downstream consumer at /v1/downstream/shopify (Cloud Run IAM). Consent is now gated twice — the gateway's registry lookup redacts person fields before ingestion, then the extender's marketing-opt-in check runs on what survives. /readyz is fail-closed on CELL33_URL, not on the webhook secret.
OAuth install flow Does not exist anywhere in the tree (grep-verified: the only OAuth code under services/ekis/ is Google Ads OAuth; no /admin/oauth reference exists in services/ or src/). §C4's "genuinely new surface" claim is measured-true for the flow itself.
Deploy path Any push to main touching services/service-marketing-connectors/** (README included) fires deploy-service-marketing-connectors.yml → production build-deploy-verify (workflow lines 4-8). Every build step below that touches this path is flagged [GATEWAY DEPLOY — deliberate] per P1_BUILD_PLAN.md §D.

1.1 Refinements to the §A / §C4 inventory (measured, not contradictions of §A's claims)

  1. §C4 lists "per-shop token custody in Secret Manager" as part of the genuinely new surface. Measured: the custody mechanism already exists (connector_credentials.py:243-371, naming mizoki-connector-{tenant}-{provider}). What is genuinely new is (a) the OAuth flow that populates it, (b) the data-plane routes reading it, (c) shop→tenant resolution. The build is smaller than the §C4 sentence implies; this design extends the existing store rather than inventing a second one (memory rule: no parallel stores).
  2. §A row 2 says "Single-tenant credential posture (Secret Manager)". Precisely: the sync path reads environment variables (main.py:55-56) that README.md:50-52 directs operators to mount from Secret Manager. Secret Manager custody is deployment posture, not code behavior; the tenant-scoped store in the same service is not consulted. §A's summary is fair; the distinction matters for the migration plan (§5.3).
  3. Tenant registry enforcement is absent from the gateway routes (table above). §A does not claim it; the production-gates list requires it; this design is where it lands.

2. Install and authorization flow [design target — IN BUILD, not started]

One MIZOKI Signal app (a single client_id/client_secret pair, platform-owned) installs into many merchant shops. Per-shop access tokens are merchant-authorized — "sanctioned ad-platform APIs with merchant tokens only" (SIGNAL_SHOPIFY_MASTER_v4.md §3.5) applies to the commerce side identically: MIZOKI holds tokens granted by the merchant, never merchant passwords, never platform-owned store credentials.

2.1 Grant type

2.2 Flow, step by step

  1. GET /shopify/install?shop={shop}.myshopify.com — entry. The shop parameter is validated against a strict ^[a-z0-9][a-z0-9-]*\.myshopify\.com$ shape before any redirect (an open redirect here is an OAuth phishing primitive). Unknown-but-valid shops are allowed — that is what installing means; the tenant row is created only at callback completion (§5.1). The console's Connect Shopify button (lane plan 2026-09-30, D5; #1285, merged 2026-10-01) is a second entry to the same step 2: the console BFF route POST /api/bff/connectors/shopify/install/start (admin role, tenant from the verified session only) calls the gateway's authenticated POST /api/v1/shopify/install/start, receives the authorize URL, and opens it only after re-checking it is the typed store's own https://{shop}/admin/oauth/authorize — the shape pin on the gateway and the host pin on the console are two independent checks on purpose.
  2. 302 to https://{shop}/admin/oauth/authorize with client_id, the §3 scope string, redirect_uri (allowlisted in the app config), and state.
  3. state nonce (CSRF): cryptographically random (≥128 bits), single-use, TTL ≤ 15 min, stored server-side keyed by nonce with the requesting shop recorded — and, for an install started from the authenticated onboarding session (POST /api/v1/shopify/install/start), the tenant it is bound to (shipped 2026-10-01, lane plan D5). Callback must present a known, unexpired, unburnt nonce whose recorded shop equals the callback shop; compare constant-time; burn on first presentation (replay of a used state ⇒ 403).
  4. Callback verification, in order (GET /shopify/callback): a. hmac query parameter: hex HMAC-SHA256 over the sorted query string minus hmac, keyed by the app client secret (doc-verified). Invalid ⇒ 401. This is a different encoding and message than the webhook HMAC (hex-over-query vs base64-over-raw-body); the design keeps two named verifiers and never conflates them. b. shop re-validated against the strict shape and the nonce's recorded shop. c. state matched and burnt (step 3). d. Only then: exchange code at POST https://{shop}/admin/oauth/access_token with client_id + client_secret.
  5. Persist, then activate: token payload written to Secret Manager (§4) first, tenant registry row created/activated (§5) second, mandatory + operational webhook subscriptions registered via Admin API third (their counts stamped on the registry row — built 2026-10-01, lane plan item 9b PR-3b), Web Pixel activated (§3, write_pixels) last. A row without a stored token must never exist (fail-closed ordering; a crash between steps leaves a re-runnable install, not a half-tenant).

2.3 Token mode: offline (expiring), and why


3. Scopes — minimal set, each justified by a named stream

Requested at install (the full set; a scope with no named stream below is a defect):

Scope Named stream that requires it Verification
read_orders orders/create, orders/updated, orders/paid, orders/cancelled and refunds/create webhooks (doc-verified: refunds/create requires read_orders) + orders bulk backfill (main.py:400-419). Streams: NCM revenue side (Rᵢ, discounts, refunds → E[RLᵢ] true-up, master §2.5), Shopify-vs-tracked reconciliation (P1 exit; reconciliation.py). (doc-verified)
read_fulfillments fulfillments/create, fulfillments/update webhooks + backfill. Stream: fulfillment status/timing for reverse-logistics true-up (E[RLᵢ]) and the §C5 fulfillments-topic envelope review. (doc-verified)
read_inventory inventory_levels/update, inventory_levels/connect webhooks (INVENTORY_TOPICS, webhook_hardening.py:267). Stream: Cell 37 MarketSignal ingest (market-level, person-free; main.py:422-447); working-capital pacing is [Roadmap] and adds no scope. (doc-verified)
read_products Products/variants bulk backfill (main.py:253-263). Stream: SKU→COGS join keys for bundle-decomposed cost (docs/onboarding/COGS_WORKSHEET.md, cogs_import.py), catalog concepts in the canonical envelope. (doc-verified: inventory_items/* topics also accept read_products)
read_customers Customers bulk backfill (main.py:264-275). Stream: identity spine for the Stage-1 ≥80% identity/attribution-coverage precondition (SIGNAL_SHOPIFY_PHASE_BINDING.md), joined on customer.id only — the shipped query selects no email/phone. shipped query, main.py:264-275
write_pixels + read_customer_events Web Pixel Extension activation and customer-event subscription (doc-verified: both scopes required to invoke web-pixel mutations). Stream: micro-signals → Cell 33 through the consent gate (services/intent-shopify-extender). (doc-verified)

Protected-customer-data posture: Level 1, not Level 2. Shopify tiers protected customer data: Level 1 = customer data excluding name, address, phone, email fields; Level 2 = including them, with field-level grants and data-protection reviews (doc-verified). P1 requests Level 1 protected-customer-data access only. Shopify then redacts/withholds the Level-2 fields at source, which composes with — and does not replace — our own fail-closed consent gate (webhook_hardening.py:140-202): Shopify's level approval is their control; the consent gate is ours; both stay.

Explicitly rejected as broader than the named streams:

Rejected Reason
read_all_orders The default 60-day order window (doc-verified; the scope needs separate Partner-Dashboard permission) covers the 14-clean-day reconciliation attestation with 4× margin. Historical seasonality baselines are P2+ and must bring their own justification then.
write_orders, write_products, every write_* except write_pixels P1 is L0/L1: no mutation authority on the store, no spend authority anywhere (P1_BUILD_PLAN.md §D; phase binding). write_pixels is the sole write, required to install our own pixel (doc-verified).
Level-2 protected fields (name/address/email/phone) P1 identity keys are shopify_customer:{id} (webhook_hardening.py:257-263). HMAC-tokenized email/phone internal keys and CAPI EMQ hashed params (master §2.4) become a separately-reviewed Level-2 request tied to the value-feed stream when it turns on (P2+, after the L1 attestation) — not before.
read_checkouts, read_draft_orders Session/checkout micro-signals come from the Web Pixel, not the checkouts API.
read_discounts, read_price_rules Pᵢ promo pro-ration reads order-level discount_applications already present in the orders payload.
read_locations inventory_levels payloads carry location_id; Cell 37 keys on ids, not location names.

Scope-set changes re-trigger merchant authorization (§2.3), so additions are visible, auditable events per shop — never silent grants.


4. Per-shop token custody [design target — IN BUILD, not started]

Extend the existing store; do not build a second one. Secret id stays mizoki-connector-{tenant}-shopify (connector_credentials.py:48, 289-293), labeled as today (:307-311). The payload becomes schema-versioned:

{"schema": 2, "shop_domain": "…", "access_token": "…", "refresh_token": "…",
 "token_expires_at": "…", "scopes_granted": ["read_orders", "…"],
 "api_version": "2026-07", "install_id": "…", "installed_at": "…"}

No credential ever appears in code, config files, images, or logs — Secret Manager only (README.md:56; memory "Production gates": Secret Manager only, least privilege).


5. Tenant registry [design target — IN BUILD, not started]

5.1 Two registries, two keys — deliberately

TENANT-001 (contracts/mizoki_contracts/tenancy.py) maps caller service-account → tenants and governs authenticated internal callers. A public webhook has no OIDC caller, so it can never be tenant-resolved by TENANT-001. The new surface maps shop_domain → tenant and is a different relation, not a parallel copy of the same one:

{"shop_domain": "…myshopify.com", "tenant_id": "…", "status": "installed|uninstalled|suspended",
 "install_id": "…", "scopes_granted": [ "…" ], "secret_ref": "mizoki-connector-{tenant}-shopify",
 "api_version": "2026-07", "country": "…", "token_state": "…",
 "installed_at": "…", "uninstalled_at": null, "redacted_at": null,
 "pixel_activation": {"status": "…", "reason": "…", "pixel_id": "…", "at": "…"},
 "first_delivery_at": null, "first_delivery_topic": null, "first_delivery_transport": null,
 "first_delivery_webhook_id": null}

The first_delivery_* fields are stamped ONCE per shop by the webhook doors after canonical ingestion accepts the first non-GDPR, non-inventory delivery (shopify_connection.py, lane plan 2026-09-30 item 9b PR-3); the health read projects them, with the row itself, into the shopify row's connection block (connector_health.connection_readiness) — a delivery verdict beside the sync verdict state. The webhook-registration count on the row (webhooks_registered / webhooks_requested) is stamped by the OAuth callback at step 5.3 (built 2026-10-01, lane plan item 9b PR-3b): the full count after a successful Admin-API registration, the partial count before a webhook_registration_failed 502; the health read reports it as recorded: true with both counts. The stamp is a field-only transactional update bound to the install that produced it (#1304 review round 1, 2026-10-02): a first-delivery stamp or an uninstall that lands while registration runs is never replaced, and a callback whose install a newer one has superseded stamps nothing (superseded, counted). A re-install rebuilds the row, so a stale count never survives it; a row installed by the Connectors-page custom-app save (which registers no webhooks) reads recorded: false, honestly. The console's Connectors card renders this block beside the sync-health row (lane plan item 13, 2026-10-02; miz-oki-command-center-ui/components/connectors/connector-health.tsx): the readiness verdict, the store record, the webhook counts and the first delivery, each named absence as its own line in the service's words — nothing inferred from credential presence, tenant_binding never shown, and onboarding_complete untouched. A health row without the block renders as before.

5.2 Webhook boundary becomes tenant-resolving

Target order in POST /webhooks/shopify (replacing main.py:491-493):

  1. HMAC verify (unchanged position — first).
  2. Replay dedup (unchanged).
  3. GDPR topics (unchanged ACK-never-ingest; §6 adds the cascade behind the ACK).
  4. Tenant resolution: X-Shopify-Shop-Domain → shopify_shop_registry: - status=installed → resolved tenant; proceed to the existing consent gate + ingest. - status=uninstalled → ACK-and-drop (§4). - no row → 503 shop_unregistered, retryable and loud: an HMAC-valid delivery for an unknown shop means registry lag or corruption, and Shopify's retry window buys repair time. - DEFAULT_TENANT_ID remains the migration shim: consulted only when the registry collection is empty (pre-multi-merchant deployments keep working). Its retirement is operator-timed after ≥1 registry row serves live traffic (AGENTS.md 6.6: dual-accept, timed removal, zero-importer check).
  5. Consent gate and canonical ingest exactly as shipped (main.py:498-537).

Honest limit, stated as posture not claim: Shopify's webhook HMAC covers the raw body only; X-Shopify-Shop-Domain is outside the signature. Forging it requires possession of a validly-signed body (only Shopify and we hold the signing secret; TLS covers transit), leaving replay-with-forged-header as the residual. Mitigations: dedup keys include the shop domain ((install_id, webhook_id)), provenance records the header verbatim for audit (main.py:525-531 already does), and compliance topics never trust the header — their payloads carry shop_domain inside the signed body (doc-verified) and §6 resolves from the body. This paragraph describes designed mitigations, not properties the current code enforces.

5.3 Sync route becomes tenant-resolving

POST /api/v1/shopify/sync (main.py:400-419) migrates in two moves:

  1. Enforce TENANT-001 where it already belongs: call resolve_tenant(caller, req.tenant_id) (contracts/mizoki_contracts/auth.py:123-157) so a mapped caller cannot act for a tenant it does not hold (403), closing the §1 gap. Unmapped callers keep the audited-passthrough migration semantics until MIZOKI_TENANT_STRICT=1 — the registry's own designed flip.
  2. Per-tenant credentials: _pull_shopify takes credentials resolved tenant → SecretManagerCredentialStore.load(tenant, "shopify") (schema 1 or 2, §4) instead of module-level env (main.py:55-56). The env pair remains the last-resort fallback during migration and is retired operator-timed (6.6 again). Expired schema-2 tokens trigger the single-flight refresh (§4) before the pull.

Tenant isolation downstream is unchanged by this note: canonical ingestion stamps and enforces tenancy on every record (main.py:176-193 forwards tenant_id; FLEET_INTEGRITY.md (b) and P1_BUILD_PLAN.md §D govern cross-tenant separation; merchant cost data and NCM outputs never cross the tenant boundary).


6. Mandatory compliance webhooks [design target on the cascade; ACK behavior is enforced today]

customers/data_request, customers/redact, shop/redact are subscribed as mandatory compliance webhooks, delivered to the same /webhooks/shopify endpoint, HMAC-verified like every other delivery.


7. Failure posture — every misconfiguration fails closed

Condition Behavior Status
Auth misconfig in a deployed env (SELF_URL/ALLOWED_CALLER_SA unset, MIZOKI_AUTH_DISABLED present) 503 on every protected route [enforced — auth.py:72-97]
Webhook signing secret unset 401 every delivery [enforced — main.py:341-348, 460-461]
OAuth client_id/client_secret unset /shopify/install + /shopify/callback ⇒ 503 shopify_oauth_unavailable — one posture-free reason on the public routes (D12); the unset or placeholder-shaped value is named on /readyz's refusal and on the authenticated /api/v1/shopify/install/start — never a dev fallback [enforced — main.py _require_oauth_config; tests/connectors/test_public_surface_disclosure.py]
Callback hmac invalid / state unknown, expired, or replayed / shop malformed 401 / 403 / 422; nonce burnt on first use [design target]
Registry has no row for an HMAC-valid shop 503 shop_unregistered (retryable, loud) [design target]
Delivery for an uninstalled shop 200 ACK-drop + audit + metric — the one deliberate non-5xx, reason in §4 [design target]
Consent registry disabled, unreachable, or erroring deny ⇒ redact person fields [enforced — webhook_hardening.py:153-183]
CELL37_URL unset for inventory topics 503 cell37_unconfigured [enforced — main.py:427-431]
CANONICAL_INGESTION_URL unset 503; /readyz fail-closed [enforced — main.py:162-165, 373-385]
Token refresh fails / refresh token lost row token_stale, pulls 503, alert; webhook verify unaffected (app-secret-keyed) [design target]
Erasure cascade leg unconfirmable 503 erasure_incomplete; job stays open [enforced at Cell 33 — cell33/ingest_cell/main.py:304; job wrapper design target]

No credential in code or config files — Secret Manager only (§4). Dev constants never deploy: the only auth bypass is inert wherever K_SERVICE is set (auth.py:33-35, 76-78) [enforced today]; the new install flow adds a readiness probe that reports shopify_oauth_configured and fails /readyz on unset or placeholder-shaped values — the measured 6c failure mode (a placeholder Klaviyo key silently no-oping, .claude/memory/active/current-priorities.md item 6) must be structurally impossible here, and the new routes are additionally gated by SHOPIFY_OAUTH_ENABLED whose OFF default is a test-asserted source literal (the FEEDS_DEFAULT_ENABLED precedent, P1_BUILD_PLAN.md §B).


8. Build plan — ordered, all [IN BUILD — not started]

Steps touching services/service-marketing-connectors/** are [GATEWAY DEPLOY — deliberate] (§1 deploy-path row). Every step ships tests in both directions per rule 01: the violation the gate must catch and the legal case it must pass.

# Step Deploy path Tests it ships with (both directions)
B0 shopify_shop_registry collection + schema-2 custody in a new gateway-local module (shopify_install.py); no contracts change none until mounted row lifecycle: activate-requires-stored-token refused when token missing / valid install activates; schema-1 payloads still load; schema-2 round-trips
B1 OAuth routes /shopify/install + /shopify/callback, flag-gated SHOPIFY_OAUTH_ENABLED (default OFF, source-literal test) [GATEWAY DEPLOY] happy path mints+stores+activates in the §2.2 order; forged callback hmac 401; replayed/expired state 403; malformed shop 422; unset client creds 503; flag OFF ⇒ routes 503; flag default asserted at the source literal
B2 Webhook tenant resolution (§5.2) with DEFAULT_TENANT_ID migration shim [GATEWAY DEPLOY] installed shop resolves; unknown signed shop 503; uninstalled shop ACK-drop with metric; empty-registry fallback keeps today's single-tenant behavior byte-identical (regression suite over PR #654 paths rerun); GDPR/consent/replay ordering unchanged
B3 Sync per-tenant credentials + resolve_tenant enforcement (§5.3); customers query drops city/zip, tolerates redacted address fields [GATEWAY DEPLOY] mapped caller × wrong tenant 403; unmapped caller audited-passthrough until strict flip; env fallback still green; expired token triggers exactly one refresh under concurrency (fake clock, single-flight); Level-1-redacted fields don't fail the mapper
B4 app/uninstalled + compliance jobs (§4, §6): deactivation, version destroy, stale-drop; customers/redact → consent revoke → Cell 33 cascade; shop/redact tenant-scoped job; customers/data_request export job [GATEWAY DEPLOY] uninstall deactivates + destroys + drops stale (and 200s them); redact enqueues with consent revoked before cascade; cascade 503 keeps the job open; ACK-never-ingest behavior provably unchanged (existing GDPR tests rerun green); re-install new install_id doesn't inherit old dedup keys
B5 Web Pixel activation at install completion (write_pixels), wiring the extender's per-shop config extender lane (separate deploy) pixel activates only for status=installed rows; consent settings honored; no activation without §2.2 step-5 completion
B6 Doc corrections riding the same deliberate landings (rule 01: fix the doc in the run that fixed the tree): README.md:97 multi-tenant guidance, README.md:50-52 env table gains the OAuth/registry entries. README is inside the deploy-trigger path — it ships with B2, not as a docs-only ride-along with B2 claims-lint clean; README states the registry path and labels the env pair as migration shim
OP [operator — AGENTS.md 7.6, not agent-executable] Create the Shopify app with direct/unlisted distribution (decision 1 ruling — no App Store listing before P4); client_id/client_secret into Secret Manager; redirect-URI allowlist; Level-1 protected-data request in the Partner Dashboard (required for protected data regardless of listing status); register-item-6 secrets (SHOPIFY_WEBHOOK_SECRET semantics become "app client secret" at B2 — coordinate the value swap with the landing); SA IAM narrowing (§4) — —

Sequencing: B0→B1→B2→B3→B4 strictly ordered; B5 after B2; B6 with B2; OP items gate live verification, not the build. Nothing turns on by default: the flag is OFF, the registry is empty, and today's single-tenant behavior is the tested regression baseline at every step.


9. What this design does NOT decide

10. Claim discipline

Everything above is design target / [IN BUILD — not started] except lines labeled [enforced — file:line], which describe code shipped on main today. Current proof levels stay exactly as recorded: the hardened webhook boundary is deployed with zero live traffic (operator secrets, register item 6); nothing Shopify-side is live-verified; no reconciliation attestation exists for any merchant. The platform ceiling remains built, pre-benchmark — this note adds no performance, security, or compliance claims, only mechanisms with named tests and the measured baseline they migrate from.

← All docsView source on GitHub →