---
title: "Architecture"
description: "Sarvex V1 is a single Go binary that runs as one of three planes (serving, sync, control), a PHP WooCommerce plugin that runs on the merchant's own server, and a Vue 3 web console for merchants and…"
---

*Generated from `sarvex-v1-architecture.md` in the [Sarvex repository](https://github.com/sarvexlab/sarvex) — edit the source there, not this page.*

> **In one sentence:** Sarvex makes a WooCommerce or Magento merchant's products transactable by AI shopping agents over the Universal Commerce Protocol (UCP) — Sarvex hosts the merchant's UCP profile and checkout endpoints, keeps their data agent-ready, and monitors the integration so it never silently breaks.
>
> **Scope authority:** this document describes the architecture **as currently implemented** in this repository. It is organised into a **Backend Architecture** part (the Go services, plugin, and shared packages) and a **Frontend Architecture** part (the Vue console), followed by explicit **Gaps**, **Risks**, and **Dependencies** sections. Where the code and this document disagree, the code wins and this document is the bug.

*Status: implementation-derived architecture · regenerated from the current tree · supersedes prior design-session drafts*

---

## Table of contents

- [Part 0 — System overview](#part-0--system-overview)
- [Part A — Backend architecture](#part-a--backend-architecture)
  - [A1. Process model & entrypoint](#a1-process-model--entrypoint)
  - [A2. The three planes](#a2-the-three-planes)
  - [A3. Serving plane](#a3-serving-plane)
  - [A4. Sync plane](#a4-sync-plane)
  - [A5. Control plane](#a5-control-plane)
  - [A6. Shared packages (`internal/pkg`)](#a6-shared-packages-internalpkg)
  - [A7. The WooCommerce plugin (PHP)](#a7-the-woocommerce-plugin-php)
  - [A8. Data stores](#a8-data-stores)
  - [A9. Event & command topics](#a9-event--command-topics)
  - [A10. Backend invariants](#a10-backend-invariants)
- [Part B — Frontend architecture](#part-b--frontend-architecture)
  - [B1. Stack & build](#b1-stack--build)
  - [B2. Routing & surfaces](#b2-routing--surfaces)
  - [B3. State: Pinia stores](#b3-state-pinia-stores)
  - [B4. API layer & type generation](#b4-api-layer--type-generation)
  - [B5. Authentication](#b5-authentication)
  - [B6. Component system](#b6-component-system)
- [Part C — Gaps](#part-c--gaps)
- [Part D — Risks](#part-d--risks)
- [Part E — Dependencies](#part-e--dependencies)

---

## Part 0 — System overview

Sarvex V1 is a **single Go binary** that runs as one of three planes (`serving`, `sync`, `control`), a **PHP WooCommerce plugin** that runs on the merchant's own server, and a **Vue 3 web console** for merchants and operators. The planes are separated by availability, latency, and trust characteristics and communicate through a NATS JetStream event bus (Watermill), never by direct in-process cross-plane calls.

```mermaid
flowchart TD
    AG["🤖 AI shopping agents<br/>(UCP)"]
    MER["👤 Merchant / Operator"]
    STORE["🏪 WooCommerce store<br/>+ Sarvex PHP plugin"]

    subgraph FE["FRONTEND"]
        CON["Vue 3 console<br/>web/console"]
    end

    subgraph BE["BACKEND (single Go binary, --service selects plane)"]
        SERV["SERVING plane<br/>internal/planes/serving"]
        SYNC["SYNC plane<br/>internal/planes/sync"]
        CTRL["CONTROL plane<br/>internal/planes/control"]
    end

    subgraph INFRA["Shared infrastructure"]
        MONGO[("MongoDB")]
        REDIS[("Redis")]
        BUS[["NATS JetStream<br/>(Watermill)"]]
        KMS["Vault<br/>(Transit + KV v2)"]
        STRIPE["Stripe"]
        KINDE["Kinde IdP"]
    end

    AG -->|"GET /.well-known/ucp"| STORE
    AG -->|"UCP API + checkout (direct)"| SERV
    STORE -->|"signals · heartbeat · profile fetch (HMAC)"| SYNC
    SERV -->|"live stock/shipping/tax/order at checkout"| STORE
    MER --> CON
    CON -->|"REST + WebSocket (bearer)"| CTRL

    SERV --- REDIS
    SERV --- MONGO
    SYNC --- MONGO
    SYNC --- REDIS
    CTRL --- MONGO
    SERV -.-> BUS
    SYNC -.-> BUS
    CTRL -.-> BUS
    SERV --- KMS
    CTRL --- STRIPE
    CTRL --- KINDE
```

**Runtime technology (from `go.mod` / `package.json`):**

| Concern | Choice |
|---|---|
| Language / runtime | Go 1.25 (backend), TypeScript / Vue 3 (frontend), PHP (plugin) |
| HTTP framework | `labstack/echo/v4` |
| CLI / config | `spf13/cobra` + `spf13/viper`, `.env` via `joho/godotenv` |
| Primary datastore | MongoDB (`go.mongodb.org/mongo-driver/v2`) |
| Cache / sessions | Redis (`redis/go-redis/v9`) |
| Event bus | NATS JetStream via `ThreeDotsLabs/watermill` (+ noop bus for unit tests; the same NATS runs locally via Docker Compose) |
| Signing / crypto | Vault Transit wraps store signing keys (`hashicorp/vault/api`), RFC 9421 HTTP message signatures (`ucpsig`) |
| Secrets | Vault KV v2 (`hashicorp/vault/api`) |
| Billing | Stripe (`stripe/stripe-go/v72`) |
| Identity provider | Kinde (`kinde-oss/kinde-go` backend verify, `@kinde-oss/kinde-auth-pkce-js` frontend) |
| Realtime | WebSocket (`gorilla/websocket`) |
| Frontend build | Vite, Pinia, Vue Router, axios, `openapi-typescript` |
| Deploy | Self-hosted Kubernetes (k3s): Helm charts + Argo CD GitOps + Cloudflare Terraform (`deployments/selfhost`); Docker Compose runs the same backing services locally (`deployments/docker-compose.yaml`) |

---

# Part A — Backend architecture

## A1. Process model & entrypoint

The backend is a **single binary with three roles**. `main.go` calls `cmd.Execute()` (`cmd/root.go`), a cobra root command that reads a required `--service` flag and starts exactly one plane:

- `--service=serving` → `servingapi.NewServingPlaneEntry(...)`
- `--service=sync` → `syncapi.NewSyncPlaneEntry(...)`
- `--service=control` → `controlapi.NewControlPlaneEntry(...)`

Config is layered by viper: `--configPath` + `--env` resolve a `configs.<env>.json` file; `--port`, `--tls` are flags. Each plane's `entry.go` wires its dependencies (datastores, bus, services) and registers Echo routes. This is why the planes cannot import each other in-process — they are separate deployables of the same binary (`internal/planes/doc.go` encodes the "never import another plane" rule).

Other `cmd/` binaries are tooling, not part of the runtime: `cmd/sarvex` (merchant onboarding driver + UCP agent / conformance runner), `cmd/sim` (a persistent fake WooCommerce store for local dev), `cmd/gateway-cli`, `cmd/verify-routes`.

## A2. The three planes

| Plane | Package | Character | Serves |
|---|---|---|---|
| **Serving** | `internal/planes/serving` | Always-on, low-latency, agent-facing | `/.well-known/ucp` profile, UCP catalog reads, checkout sessions |
| **Sync** | `internal/planes/sync` | Event-driven, eventually consistent, retryable | Connector ingest (`/connector/*`), catalog import/scoring, cache rebuild, surface artifacts |
| **Control** | `internal/planes/control` | Interactive, authenticated humans, iterates daily | Console/operator REST API + WebSocket |

The **plane boundary is the load-bearing invariant** (`internal/planes/doc.go`, `sarvex-v1-architecture.md` §4.1 in prior drafts). Cross-plane communication is exclusively through the event bus. Mechanical enforcement via linter is documented in [ADR-0004](/contributing/adr/0004-plane-boundary-enforcement-via-depguard).

## A3. Serving plane

**Package:** `internal/planes/serving` (+ `.../serving/api` for HTTP).

**HTTP surface** (`serving/api`, from route registration):

| Method + path | Purpose |
|---|---|
| `GET /.well-known/ucp` | The UCP business profile (per store). Served with the hosting rules the spec mandates. |
| `POST /catalog/search` | Agent catalog search (cache-only). POST per the reconciled ucp.dev REST transport (`search_catalog`), not GET+query params. |
| `GET /catalog/:itemId` | Agent product lookup (cache-only). |
| `POST /checkout-sessions` | Create a checkout session (live stock + pricing). |
| `GET /checkout-sessions/:checkoutId` | Read current session state. |
| `PUT /checkout-sessions/:checkoutId` | Full-replacement update (buyer / fulfillment / selection); live shipping + tax. |
| `POST /checkout-sessions/:checkoutId/complete` | Accept opaque payment credential + handler, create the real order, return the order. |
| `POST /checkout-sessions/:checkoutId/cancel` | Cancel a non-terminal checkout session. Repeated cancellation is idempotent success; cancelling a completed/failed session is a conflict. |
| `GET /orders/:orderId` | Read the order a checkout session's `complete` placed — line items, fulfillment expectations/events, adjustments, totals. |
| `PUT /orders/:orderId` | Append a fulfillment event and/or an adjustment. Both are append-only logs; resending a known ID is a no-op. |

The checkout HTTP surface is now the five operations earlier design drafts described (create/get/update/complete/cancel) — closing **G1** (below). Orders is a new resource on top of the same checkout session record: a completed `CheckoutSession` doubles as Sarvex's record of the order it placed (`serving/order_service.go`), rather than a second, separately-owned store — `GetByOrderID` (a secondary index alongside the session store's existing idempotency-key index) is the read path both `GET /orders/:id` and the order-negotiated capability (below) depend on.

**Why orders exists:** the `dev.ucp.shopping.order` capability was not part of V1's original checkout-first cut, but the official UCP conformance suite's reference shopping agent declares it unconditionally on *every* negotiated request (its fixture profile carries no other capability) — so its absence failed capability negotiation for checkout itself, not just order-specific calls. Declaring it required a route behind it, per the same rule that had kept `cart`/`identity_linking` out of the V1 profile (`internal/pkg/ucp/catalogue/loader.go`'s `v1ImplementedCapabilities`).

**Components:**

- **Edge/handler + pipeline** (`api/handler.go`, `api/pipeline.go`, `api/dependencies.go`) — terminates agent requests, resolves the store, verifies/signs via `ucpsig`, runs negotiation.
- **Profile renderer + source** (`profile_renderer.go`, `profile_source.go`) — projects the definitions catalogue + per-store data into the rendered UCP profile.
- **Serving cache** (`cache.go`, `cache_redis.go`) — per-store versioned snapshots in Redis; atomically swapped by the rebuilder.
- **Rebuilder** (`rebuilder.go`) — consumes `serving.*` rebuild events and swaps snapshots.
- **Catalog source** (`catalog_source_mongo.go`, `catalog_source_stub.go`) — read model for catalog data.
- **Checkout session store** (`session.go`, `session_store.go`, `session_memory.go`, `session_redis.go`) — stateful sessions + idempotency, Redis-backed with in-memory fallback.
- **Request log emitter** (`reqlog.go`) — fire-and-forget `serving.log.requested`.
- **Consumer** (`consumer.go`, `events.go`) — bus subscriptions.

**Invariant enforced here:** discovery/read paths (`profile`, `catalog`) read **only** the serving cache. Checkout is the deliberate, spec-mandated exception that calls the merchant backend live (via the sync-plane connector) for stock, shipping rates, tax, and order creation.

## A4. Sync plane

**Package:** `internal/planes/sync` (+ `.../sync/api`, `.../sync/connector`, `.../sync/feed`).

**HTTP surface** (`sync/api`, HMAC-authenticated plugin traffic):

| Method + path | Purpose |
|---|---|
| `POST /connector/signals` | Thin change signal from the plugin (type, id, seq, HMAC). |
| `POST /connector/heartbeat` | Plugin/connector health + environment. |
| `GET /connector/profile` | Serves the current profile bytes to the plugin (Role 2 doorway source). |

**The pipeline** (`catalog change → normalise → map → score → issues`), single-writer per store:

- **Ingestion** (`ingestion.go`) — consumes `sync.connector.signal_received`, de-dups (Redis-backed per [ADR-0007](/contributing/adr/0007-signal-dedup-window-persistence)), orders per store, then *pulls the full entity* (thin events, fat pull) through the connector.
- **Resolve/mapping/ontology** (`resolve.go`, `mapping.go`, `ontology.go`, `ontology_mongo_test.go`) — maps raw attributes onto canonical product fields via the ontology (rule + learned tiers; **no LLM tier**).
- **Scoring** (`scoring.go`, `scoring_rubric.go`, `readiness.go`, `readiness_store.go`) — evaluates the mapped product against the loadable, versioned **scoring rubric**, producing `ReadinessResult{ overall, dimensions, issues[] }`. Dimensions: completeness, consistency, attribute quality, delivery data. There is exactly one score stage (`runScoreStage` → `ScoreAndPersist`); live changes, bulk import, sweep corrections and the audit command all go through it, so they cannot disagree. A product is agent-ready when no issue blocks checkout (zero blockers) — there is no score floor. **Surface scoping:** attribute fields and rules carry an optional `surface` tag (empty = core, applies to every store). The sync plane reads `control_store_surfaces` (read-only) and resolves/scores a store only against core plus the surfaces it is on (any enrollment state but `available`), so a surface's own attribute vocabulary is neither shown nor scored for a store that is not on that surface. The control plane asks sync to rescore (the existing `request_audit` command) when a store enters or leaves a surface. **Ontology:** the rule-tier mappings (`catalogue/v1/ontology_seed_data.go`) are versioned catalogue content that sync — the ontology's one writer — upserts into `sync_ontology` at startup (seed-prefixed ids only; learned rules are untouched).
- **Eligibility** (`eligibility_rules.go`) — UCP-eligibility rules feeding `blocks_checkout`.
- **Consistency engine** (`consistency_engine.go`) — detects divergence across sources; emits `sync.reconciliation.discrepancy_found`.
- **Catalog store** (`catalog_store.go`, `catalog_mongo.go`, `product.go`, `order.go`, `order_mongo.go`) — the working truth, partitioned by `store_id`.
- **Catalog import** (`catalog_import.go`, `import_job.go`, `import_job_mongo.go`, `job_server.go`) — initial + resync full imports, driven by the `control.catalog.import` command; emits `sync.catalog.import_progress`.
- **Rebuilder inputs** (`rebuilder_builder.go`, `rebuilder_transform.go`) — builds product snapshots, emits `sync.catalog.products_ready`.
- **Sweep** (`sweep_handler.go`) — scheduled reconciliation full-pulls as the safety net for dropped signals.
- **Order intent** (`checkout_order_intent.go`, `order.go`) — records the order the serving plane already created at `complete` (`serving.checkout.order_intent` → `sync.order.created`); models order status + sync state.
- **Connector registry** (`sync/connector`) — the outbound store-API client (`ucp.Connector` implementations) with scoped credentials, retry/backoff, circuit breaking.
- **Surface artifacts** (`sync/surface`) — the sync plane's half of an agent surface. A `Generator` renders one surface-scoped artifact for a store (no bundled surface generates one today; a surface that does registers its `Generator` in `sync/surface/all`); the `Runner` generates it only for stores that have started enrolling on the surface (it reads `control_store_surfaces`, read-only), on enrollment, on catalog change (debounced) and on an hourly safety net. The bytes go to the serving cache under an unguessable token and a record to `sync_surface_artifacts` (sync is its one writer). The serving plane serves `GET /artifacts/{token}` cache-only and reports each fetch on the bus (`serving.surface_artifact.fetched`, token only); the runner attributes it and records the surface's first-fetch signal, then emits `sync.surface_artifact.updated`, which the control plane's surface subscriber answers by re-evaluating that enrollment. Withdrawal unpublishes. Nothing in the runner names a surface.

## A5. Control plane

**Package:** `internal/planes/control` (+ `.../control/api` for HTTP). Each subdirectory is a service; `api/handler.go` wires them via an injected `Dependencies` struct.

**Services** (`internal/planes/control/*`):

| Service | Responsibility |
|---|---|
| `account` | Merchant accounts, team members, invites (email sign-in links via `mailer.go`), locales. |
| `auth` | Principal resolution: **Kinde JWT bearer verification** (`bearer.go`) + connector **HMAC** (`hmac.go`). |
| `store` | Store lifecycle under a merchant (`merchant → store`), store settings, profile. |
| `connector` | Connector/pairing lifecycle (single writer of the pairing record), resync trigger. |
| `catalog` | Catalog reads, readiness, remediation apply/revert, audits, write-backs. |
| `task` | The one task spine (detected + prescribed tasks), owner (`you`/`us`/`external`), resolve. |
| `journey` | The operator-facing concierge funnel (task reconciliation, websocket push, board stage) over the pure lifecycle: `prospect → connected → synced → ready → live`. Descriptive, not decisive — see `activation`. |
| `activation` | **The store's own UCP activation lifecycle — the actual truth of whether a store is live.** `draft → connector_linked → profile_published → verified → active` (+ `suspended`), driven by `endpointhealth`'s probes. Zero dependency on any agent surface: a store reaches `active` with none enrolled. |
| `surface` | Generic, registry-driven agent-surface enrollment (per-store, zero-to-many) — `available → prerequisites_pending → configured → submitted → verifying → enrolled`. Registered (`surface/all`): `directagent` (zero-config); further surfaces add themselves with one line. A descriptor may declare a typed `config_schema` the console renders as a form and `SetConfig` validates; a surface with none keeps free-form config. Artifacts and verification signals generated by a surface's sync half are read back through the generic synced-artifact adapters (`surface/synced.go`). See `docs/HOWTO-add-surface.md`. Depends one-way on `activation` (enrollment can't progress past `available` until the store is `active`), never the reverse. |
| `engagement` | Operator-facing display labels for `journey`'s 5 stages (name, description, typical duration) — descriptive metadata only, `journey`'s transition table owns every move. |
| `validation` | Validation runs + conformance records, spec-pinned. |
| `monitoring` | Semantic probes (`probe.go`), monitoring `loop.go`, consistency + discrepancy subscribers. |
| `billing` | Stripe integration, plans, portal config, webhook handling. |
| `operator` | Cross-merchant board, act-on-behalf, audit log, manual-step capture. |
| `facts` | Merchant business-facts capture. |
| `realtime` | WebSocket hub fanning `control.journey.updated` to open console sessions. |
| `domain` | Shared control-plane domain types (Principal, StoreID, etc.). |
| `engagement`, `sim`, `integration` | Engagement metadata, simulator glue, integration-test harness. |

**HTTP surface** (control plane, `sarvex-v1-control-plane.yaml` generated from Go code per [ADR-0009](/contributing/adr/0009-control-plane-api-source-of-truth); `merchant_id`/`store_id` are **never** URL params for tenant scope — resolved from the authenticated principal; path segments use hyphenated casing per [ADR-0006](/contributing/adr/0006-control-plane-path-segment-casing)):

- **Account/identity:** `GET/PATCH /account`, `GET /account/members`, `POST /account/members`, `GET/PUT /notifications/preferences`.
- **Stores:** `GET/POST /stores`, `GET/PATCH/DELETE /stores/:store_id`, `GET/PUT /stores/:store_id/profile`.
- **Connectors/pairing:** `GET /connectors`, `GET/DELETE /connectors/:connector_id`, `POST /connectors/:connector_id/resync`, `POST /stores/:store_id/connectors/pairing`, `POST /connectors/pairing/complete`.
- **Catalog/readiness:** `GET /stores/:store_id/catalog/products[/:sku]`, `.../catalog/readiness`, `.../catalog/remediations`, `POST .../catalog/audits`, `POST .../catalog/remediations/apply`, `POST .../catalog/write-backs/:write_back_id/revert`.
- **Journey:** `GET /stores/:store_id/journey`, `GET .../journey/stream` (WebSocket).
- **Health/monitoring:** `GET /stores/:store_id/health`, `.../health/consistency`.
- **Presence (UCP profile view):** `GET /stores/:store_id/presence`, `.../presence/manifest`, `POST .../presence/rebuild`.
- **Orders:** `GET /stores/:store_id/orders[/:order_id]`.
- **Payment config:** `GET/PUT /stores/:store_id/payment-config`.
- **Validation:** `GET /stores/:store_id/validation/status`, `GET .../validation/runs`, `POST .../validation/runs`.
- **Tasks:** `GET /tasks[/:task_id]`, `POST /tasks/:task_id/resolve`.
- **Billing:** `GET /billing`, `POST /billing/portal-link`, `POST /billing/webhooks/stripe`.
- **Operator (elevated scope):** `GET /operator/board`, `GET /operator/audit-log`, `GET /operator/timeline-metrics`, `GET/POST /operator/merchants`, `GET/POST /operator/merchants/:merchant_id/members`, `GET/POST /operator/merchants/:merchant_id/stores`, `POST /operator/act-on-behalf`, `GET/POST /operator/manual-steps`, `PUT /operator/manual-steps/:step_id/retag`, `POST /operator/manual-steps/bulk-retag`.

## A6. Shared packages (`internal/pkg`)

The UCP protocol machinery lives in `internal/pkg/ucp` and is shared by all planes:

- **`internal/spec/vYYYYMMDD`** — one module per UCP spec generation Sarvex models: `v20260408` and, as of the 2026-08-25 spec release, `v20260825`. Each holds that generation's deep schema types (capabilities, payment, fulfillment, messages, enums, and `extensions/` — `buyerconsent`, `fulfillment`, `discount`, `ap2`; see its own `DISCREPANCIES.md`), its profile document form (`wire`), its `generation.Generation` (`profile`), its `wireadapt.Codec` (`codec`: checkout/cart/order translation and envelope, catalog search/lookup) and its `spec.Version` (`release`: id, served capabilities, codec, profile). No generation imports another. `internal/spec` is the one registry (`Latest`, `Baseline`, `Serving`, `Servable`, `Codecs`, `Generations`), assembled in `internal/spec/all` — adding a generation means a new module and one line there; nothing else changes, which a canary generation in the tests proves.
- **`ucp/wire`** — the generation-neutral profile constructs (`Profile`/`UCP`/`Capability`/`Service`/`PaymentHandler`/`JWK`), the same in every generation Sarvex serves. A generation's profile document differs only in the name of its key set (`signing_keys`, then `keys`); each `internal/spec/vYYYYMMDD/wire` holds its own document form, and a profile is decoded leniently from either name.
- **`ucp/wireintersect`** — the UCP capability intersection algorithm (name+version selection, orphan pruning to fixpoint), implemented once, generically, over any generation's wire `Capability` type — not duplicated per generation.
- **`ucp/generation`** — the profile-document interface (its registry is the spec registry's `Generations()` view) pairing each `types.Version` with its profile document: `DecodeProfile` (dispatched by sniffing a raw profile's declared `ucp.version`) and `EncodeProfile`, which writes Sarvex's own profiles — the published business profile's keys, the endpoint-health probe's reference profile — in the form of the generation they declare. Each generation's implementation is its `internal/spec/vYYYYMMDD/profile` package.
- **`ucp/ucpsig`** — RFC 9421/9530 HTTP message signing/verification.
- **`ucp/ucpnego`** — a largely-unused reference/example implementation of the negotiation protocol (`profile`, `negotiate`, `httpx`/`echox` transport, worked `examples`) — not the production path; see `ucp/negotiation` for that. Its `negotiate` package's multi-parent-extension pruning rule (any parent present survives) disagrees with `ucp/negotiation`'s (all parents must survive) — a pre-existing inconsistency between the two, not yet reconciled.
- **`ucp/negotiation`** — negotiation wiring used by the serving plane; the production negotiation engine. Not yet generation-registry-aware: `ValidateVersion` still gates on a single business-declared version string per store (today always 2026-04-08). Wiring it to `ucp/generation` so a request can be dispatched to either generation is scoped, follow-up work — it is not safely separable from making the catalog/checkout response-building path generation-aware too (an agent negotiated into a generation Sarvex then serves in the *other* generation's shape is worse than not negotiating at all).
- **`ucp/catalogue`** (+ `catalogue/v1`) — the **definitions catalogue**: supported capabilities/extensions/payment handlers, attribute fields, business-facts definitions, and the **scoring rubric** — loadable, versioned content (`loader.go`, `seeder.go`, `catalogue_data.go`, `scoring_rubric_data.go`). Still single-generation (v20260408) as of this writing; becoming a per-generation registry mirroring `ucp/generation` is scoped, follow-up work once a v2026-08-25 catalogue data set is authored.
- **`ucp/connectors`** — `base` + `woocommerce`, `shopify`, `magento` store-API clients.
- **`ucp/validator`** — per-platform validators (`woocommerce`, `shopify`, `magento`) + `registry`.
- **`ucp/paymenthandler`** — handler adapters: `googlepay`, `shoppay`, `stripe`, `adyen`, `braintree`, `paypal` (broader than the design's google_pay/shop_pay pair).
- **`ucp/resilience`** — retry / circuit-breaker helpers.
- **`ucp/event_types.go`, `ucp/command_types.go`** — the **single source of truth for bus topic names**.

Cross-cutting infra packages: `eventbus` (NATS JetStream + noop + envelope), `commandbus`, `domainevents`, `devinfra` (local/test addresses of NATS and Vault), `secretmanagement`, `jwkmanagement`, `keypair`, `email` (+ templates), `middlewares`, `retry`, `concurrency`, `simstore`, `contracts`, `domain`, `common`, `utils`.

## A7. The WooCommerce plugin (PHP)

**Location:** `plugin/woocommerce` (`sarvex-ucp-gateway.php` + `src/`). The plugin stays thin — it processes nothing and decides nothing. Two roles:

- **Role 1 — signalling (outbound).** `SignalSender.php` fires HMAC-signed (`HMAC.php`) change signals to the sync plane's `POST /connector/signals` with a hard timeout and local retry. *Thin events, fat pull* — Sarvex pulls the full entity.
- **Role 2 — profile doorway (inbound).** `ProfileProxy.php` serves `/.well-known/ucp` on the merchant's own domain by fetching Sarvex's bytes (`GET /connector/profile`) and caching last-known-good. It does **not** proxy the UCP API — the profile's `services.endpoint` points directly at Sarvex.
- **Pairing** (`PairingManager.php`) — one-time pairing code exchange for the HMAC secret + scoped store REST credentials.

Supporting: `Plugin.php`, `Autoloader.php`, `Logger.php`; tests under `plugin/woocommerce/tests`.

## A8. Data stores

| Store | Backend | Written by | Character |
|---|---|---|---|
| Merchant registry (accounts, stores, members, connector config + scoped creds, payment config) | MongoDB | Control plane | Tenant root; `merchant → store → connector` |
| Catalog store (normalised products, mappings, scores, findings, orders) | MongoDB | Sync plane | Partitioned by `store_id` |
| Ontology library (rule + learned mappings) | MongoDB | Sync mapping workers | The one deliberately cross-tenant store |
| Definitions catalogue (capabilities, extensions, handlers, scoring rubric) | Loadable content (`catalogue/v1`, seeded to Mongo) | Spec-reconciliation | Cross-tenant, versioned content |
| Serving cache (per-store snapshots) | Redis | Sync rebuilder → Serving | Read-optimised, atomically swapped |
| Checkout sessions + idempotency | Redis (memory fallback) | Serving | Short-lived, strongly consistent |
| Import jobs / feed status | MongoDB | Sync | Job progress |
| Task / journey / conformance / audit / billing projection | MongoDB | Control services | The console's backbone |
| Event log | `serving.log.requested` → bus/sink | All planes | Append-only |
| Secrets (store creds, handler config, signing key refs) | Vault (KV v2 / Transit) | Control | Never in DB in raw form |

**ID generation:** Entity identifiers use different schemes based on entity type and lifecycle. See [ADR-0008](/contributing/adr/0008-entity-id-generation-schemes) for the selection matrix and rationale.

## A9. Event & command topics

Topic names are constants in `internal/pkg/ucp/event_types.go` / `command_types.go` (the single source of truth). Observed topics:

- **Commands (control → sync):** `control.catalog.import`, `control.connector.resync_requested`, `control.feed.regenerate_requested`, `global.request.audit` (re-score a catalog), `global.apply.remediation` (apply a write-back's fixes: product, store, records marked applied with each field's prior value) and `global.revert.remediation` (undo a write-back: restore each prior value on the product and in the store, reopen the records).
- **Journey:** `control.journey.signal` (facts in), `control.journey.updated` (state out, fanned to WebSocket).
- **Sync events:** `sync.connector.signal_received` (the one intentional intra-sync bus hop), `sync.catalog.import_progress`, `sync.catalog.products_updated`, `sync.catalog.products_ready`, `sync.task.raised`, `sync.reconciliation.discrepancy_found`, `sync.order.created`.
- **Serving events:** `serving.checkout.order_intent`, `serving.log.requested`, `serving.snapshot.swapped`, `serving.rebuild.requested`.
- **Store:** `control.store_profile_updated`.

The bus is reserved for hops that cross a plane boundary (plus `sync.connector.signal_received`, which decouples the HTTP request path from per-store-ordered ingestion). Most intra-plane hand-offs are direct Go calls.

## A10. Backend invariants

These are enforced in code and are stop-and-report conditions if violated:

1. **Three planes, isolated.** No plane imports another (`internal/planes/doc.go`). Cross-plane = bus only.
2. **Discovery/read serves only the cache.** Serving profile/catalog never call the store or control plane live. Checkout is the one spec-mandated live exception, and only through the sync-plane connector.
3. **One writer per store.** Each store's catalog/serving data has exactly one writing component; everyone else consumes events.
4. **Two-level tenant isolation.** Account data by `merchant_id`, catalog/serving data by `store_id`; every store belongs to one merchant. `merchant_id`/`store_id` are resolved server-side from the bearer claim / HMAC credential / authorized operator scope — **never URL params**. The only cross-tenant stores are the ontology + definitions catalogue. Enforcement pattern documented in [ADR-0005](/contributing/adr/0005-tenancy-enforcement-via-query-level-fencing).
5. **Authentication is external.** Kinde issues the JWT (OAuth 2.0 auth-code + PKCE); Sarvex services only **verify** (issuer, audience, JWKS signature, expiry) and read identity + scope. No passwords, no sessions, no login/logout endpoints minted by Sarvex. Connector traffic uses HMAC, not the JWT.
6. **Idempotency at every external edge.** Inbound signals de-dup by sequence; checkout mutations require idempotency keys; order recording is idempotent.
7. **Spec version is data.** Scoring rubric, definitions catalogue, and required-attribute sets are versioned loadable content; conformance records pin the version.
8. **Payment-credential boundary is structural.** The checkout schema has no field able to store a raw credential. Sarvex sees opaque references + `handler_id` only.

---

# Part B — Frontend architecture

**Location:** `web/console` — the merchant + operator console. Package `@sarvex/console` (v0.2.0).

## B1. Stack & build

- **Framework:** Vue 3 (`<script setup>` SFCs) + TypeScript.
- **Build/dev:** Vite (`npm run dev` on :5173; `make console` runs mock-mode with no backend, `make console-real` proxies `/api` → control plane :8081).
- **State:** Pinia.
- **Routing:** Vue Router.
- **HTTP:** axios (`src/api/client.ts`).
- **Types:** `openapi-typescript` generates `src/api/schema.d.ts` from `sarvex-v1-control-plane.yaml` (`npm run gen:api`) — the frontend consumes the **same contract** as the backend.
- **Test:** Vitest + `@vue/test-utils` + jsdom (co-located `*.test.ts`).

## B2. Routing & surfaces

Routes (`src/router`) map to view "surfaces" (`src/views`). Auth/operator guards run in a navigation guard; a `shell` meta toggles the app chrome; a connect-gate redirects unpaired stores to `/connect`.

| Path | View | Meta |
|---|---|---|
| `/login` | `LoginView` | public, no shell |
| `/connect` | `ConnectSurface` | pairing gate |
| `/path` (default) | `PathSurface` | the journey/task spine |
| `/store` | `StoreSurface` | catalog, products, orders, presence |
| `/health` | `HealthSurface` | monitoring + consistency |
| `/checkout-sessions/:checkoutId` | `CheckoutSessionDetailView` | checkout escalation detail (drill-down from the Analytics surface's Checkouts tab) |
| `/validation` | `ValidationView` | validation runs/status — not in `NAV`; superseded by `/path` |
| `/analytics` | `AnalyticsSurface` | tabbed surface (`?tab=`) over Checkouts (`CheckoutsPanel`), Lost Revenue (`LostRevenuePanel` — `GET /ledger/summary`, `/ledger/entries`), Channel Dashboard (`ChannelPanel` — `GET /channel/*`), and Agent Behavior Insights (`AgentInsightsPanel` — `GET /insights/*`); the Lost Revenue/Channel/Insights tabs are analytics-tier gated |
| `/account` | `AccountHomeView` | account + members + billing |
| `/settings` | `SettingsSurface` | account/store settings drawer |
| `/board` | `BoardSurface` | **operator only** — cross-merchant board |
| `/board/:merchantId/:storeId` | `EnterAsMerchantView` | **operator only** — act-on-behalf |
| `/_/styleguide` | `StyleguideView` | design-system reference |

Within `/analytics`, the Lost Revenue, Channel, and Insights tabs are billing-gated: each panel always renders, but `TierGate.vue` shows a locked panel (with an upgrade CTA to `/settings`) until `billing.plan` is on the analytics tier (`lib/*.ts`'s `isAnalyticsTier`, mirrored server-side by the OpenAPI's `requireAnalyticsTier`). Routing never hides the page — the tier check is a content gate, not a nav gate.

`/validation` is deliberately not linked from `NAV`: its content already lives on `/path` — `JourneyBar`'s stage nodes preview journey progress — so a separate nav destination would duplicate the Path spine. The route stays live (deep links, `StoreSwitcher`'s stage badges, etc.) but is reached by URL, not the top nav.

The generic agent-surface enrollment flow (`SurfacesSurface`) is built but not routed: with only the zero-config `direct_agent` surface there is nothing to enroll in. A surface that needs enrollment adds its descriptor, its console copy folder (`src/surfaces/<id>`) and a route.

## B3. State: Pinia stores

`src/stores` — one store per backend domain, each backed by the matching `src/api` module: `account`, `auth`, `billing`, `board`, `catalog`, `channel`, `connectors`, `health`, `insights`, `journey`, `ledger`, `notifications`, `operator`, `orders`, `payment`, `presence`, `storeProfile`, `stores`, `tasks`, `theme`. `stores.ts` holds the active-store selection that scopes store-scoped calls.

## B4. API layer & type generation

`src/api` wraps the control-plane REST contract: `client.ts` is the axios instance (bearer injection, base URL / proxy), and one typed module per resource (`account`, `billing`, `catalog`, `channel`, `connectors`, `health`, `insights`, `journey`, `ledger`, `orders`, `payment`, `presence`, `remediations`, `storeProfile`, `stores`, `tasks`, `validation`, `operator`, `notifications`). `mocks.ts` + `fixtures.ts` provide a full mock backend so the console runs standalone (mock mode). All request/response shapes are the generated `schema.d.ts` types.

## B5. Authentication

`src/auth` implements a provider abstraction (`provider.ts`) with two modes selected by `VITE_AUTH_MODE`:

- **`kinde`** — real Kinde Authorization Code + PKCE via `@kinde-oss/kinde-auth-pkce-js` (lazy-imported so simulator builds don't carry it).
- **`simulator`** (default) — mints a JWT-shaped token locally for offline/dev work.

The provider exposes login/logout/token so `client.ts` attaches the bearer and the router guard reads auth + operator status. This mirrors the backend's "external IdP, verify only" model.

## B6. Component system

`src/components` is organised by area:

- **`ui`** — the design-system primitives (`Chip`, `Field`, `Switch`, `Icon`, `OwnerChip`, `CopyField`, `LoadingButton`, `ErrorBanner`, `Skeleton`, `TierGate`).
- **`shell`** — app chrome (`AppShell`, `StoreSwitcher`, `UserMenu`, `ThemeToggle`, `ReadinessPill`, `Mark`).
- **`journey`** — `JourneyBar` and per-stage cards under `journey/stages`. (`FeedStepCard` is archived — see `web/console/archive/README.md`.)
- **`task`** — `TaskFeed`, `TaskCard`, `TaskDrawer`.
- **`connect`** — pairing steps (`ConnectIdleStep`, `ConnectPairingStep`, `ConnectPairedStep`).
- **`store`** — `StoreProductsPanel`, `StoreOrdersPanel`, `StorePresencePanel`, `ProductDrawer`, `ManifestView`.
- **`operator`** — `OperatorBar`, `MerchantCard`, `NewMerchantModal`, `AddStoreModal`, `ManualStepModal`, `AuditLog`.
- **`settings`** — `SettingsDrawer`, `AccountSettingsPanel`, `StoreSettingsPanel`.
- **`ledger`** — `LedgerSummaryHeader`, `LedgerCauseBreakdown`, `LedgerEntriesTable`.
- **`channel`** — `ChannelKpiRow`, `ChannelFunnel`, `ChannelTrendChart`, `ChannelComparison`.
- **`insights`** — `CapabilityGapsSection`, `NegotiationSection`, `ProductDemandSection`, `AttributeGapsSection`.

`src/lib` holds pure helpers (`datetime`, `issue`, `plans`); `src/styles` holds the theme.

---

# Part C — Gaps

Where the implementation is incomplete relative to the intended V1 design (each cross-checked against the code):

- ~~**G1 — Checkout `cancel` operation missing.**~~ **Closed.** `POST /checkout-sessions/:id/cancel` now exists (`serving/checkout_service.go`'s `Cancel`), alongside the new `dev.ucp.shopping.order` capability and `GET/PUT /orders/:id` (A3, above) — added together once the official UCP conformance suite's negotiation requirement surfaced the order-capability gap.
- **G2 — Outbound order status-sync loop not wired end-to-end.** `sync/order.go` models `OrderStatus` and `OrderSyncState{created,synced,failed}` and `checkout_order_intent.go` records the order created synchronously at `complete`, but there is **no order-handoff/status-sync worker** driving confirmed → shipped → cancelled/refunded updates back toward the originating agent platform. The reverse-direction lifecycle sync is modelled, not operating.
- **G3 — Merchant-facing task/alert email not sent.** Email templates exist (`notification.*.tmpl`, `task_notification.*.tmpl`) and `GET/PUT /notifications/preferences` is live, but only the **member-invite** mail path (`account/mailer.go`) is wired to actually send. No code consumes `sync.task.raised` to notify the merchant by email; notification preferences currently gate nothing.
- **G4 — Identity-linking capability represented but unimplemented.** `dev.ucp.shopping.identity` (OAuth 2.0 buyer linking) is present in the catalogue surface but there is no auth/token flow implementation — consistent with the design's "defer to first need," but a gap against the full surface.
- **G5 — IdP is Kinde.** Both backend (`kinde-oss/kinde-go`) and frontend (`@kinde-oss/kinde-auth-pkce-js`) use **Kinde**; any other IdP reference in older material is stale.
- **G6 — Magento connector is scaffolded, not proven.** `internal/platform/magento` (connector + `validator`) exists, but WooCommerce is the only end-to-end-exercised platform (scenarios, plugin, E2E journey). Magento is "late-V1" and unverified.
- **G7 — Cross-tenant listing is the only multi-store affordance.** The model is keyed `merchant → store` and the operator board lists a merchant's stores, but there is no merchant-facing multi-store management UI (by design for V1) — noted so it isn't mistaken for missing tenancy support.
- ~~**G8 — Google Merchant Center surface.**~~ **Removed** from the open-source release (kept only as history under `internal/archive/gmc`). The generic surface framework and `direct_agent` remain.

> Verify each gap against the current tree before building on it — this list is derived from a read of the code at regeneration time, not a maintained backlog.

---

# Part D — Risks

Architectural and operational risks implied by the current design/implementation:

- **R1 — Checkout depends on a third party's live server.** By design, checkout calls the merchant backend synchronously for stock/shipping/tax/order creation. A slow or down WooCommerce store *can* and *should* fail a checkout cleanly — but this makes the connector's latency budget and circuit breaker (`ucp/resilience`, `sync/connector`) **money-path-critical**, not sync-plane hygiene. A regression there directly fails real transactions on Google's surface.
- **R2 — Serving-plane availability is the whole product's SLA.** Because the profile edge and checkout live in the serving plane, any coupling that lets a control-plane or sync-plane failure reach serving would violate the core promise. The plane isolation invariant (A10.1/A10.2) must be actively defended in review; it is the single most consequential invariant.
- **R3 — Spec drift.** Materialized: ucp.dev published a breaking-change release (2026-08-25) after Sarvex was built against 2026-04-08. Mitigation is now partially real rather than aspirational — `ucp/types`, `ucp/wire`, and `ucp/generation` are a working per-generation registry (§A6), with both generations' deep schema types, envelopes, and negotiation-decode paths landed and tested side by side, proven not to interfere with each other. What remains before Sarvex can actually transact in the new generation: `ucp/catalogue` is still single-generation; `ucp/negotiation`'s per-request version gate still only recognizes one business-declared version; and the serving-plane response-building path (`profile_renderer.go`, `catalog_contract.go`, `checkout_contract.go`) and the connector/checkout money-path (`connectors/*`, `checkout_service.go`) are still hardcoded to `v20260408` types throughout. These three are coupled — none is safely finishable alone — and are the scoped follow-up work. A *third* future spec release is now materially cheaper to absorb than the first one was: the registry pattern established here means it needs a new `types`/`wire`/`generation` package trio, not a redesign.
- **R4 — Payment-handler config surface is broad and secret-bearing.** Six handler adapters (`googlepay`, `shoppay`, `stripe`, `adyen`, `braintree`, `paypal`) each carry processor/gateway secrets. Mis-scoping the secret boundary (Secret Manager/KMS) or leaking handler config into the profile/cache is a high-severity exposure. The credential boundary is structural for buyer instruments, but handler *configuration* is genuinely sensitive.
- **R5 — Dropped signals rely on the sweeper.** Plugin signals are fire-and-forget with local retry; the reconciliation sweeper is the only safety net. If the sweep cadence is too slow (or disabled), price/stock drift can persist past the freshness window and produce disputed transactions before detection.
- **R6 — Order status-sync gap (G2) can silently strand orders.** Until the status-sync loop is operating, a Sarvex-created order whose merchant-side status changes (cancelled/refunded/shipped) will not propagate back — a correctness risk for the order capability that monitoring must cover in the interim.
- **R7 — Auth-mode footgun in the console.** The frontend defaults to the **simulator** auth provider when `VITE_AUTH_MODE` is unset. A misconfigured deployment could ship simulator auth to a real environment; build/release config must assert `kinde` for anything non-local.
- **R8 — Bus-only cross-plane coupling shifts failure modes to delivery semantics.** Correctness now depends on JetStream delivery/ordering guarantees and idempotent consumers. At-least-once delivery + the noop bus used in unit tests mean those tests may not exercise real ordering edge cases; consumer idempotency must be treated as load-bearing, not incidental.

---

# Part E — Dependencies

External systems, libraries, and internal build/order dependencies the architecture relies on:

**External services (runtime, third-party):**

| Dependency | Used by | Purpose | Failure impact |
|---|---|---|---|
| **Kinde** | Control plane (verify), Console (login) | Identity provider (OAuth 2.0 + PKCE), JWKS | No new sign-ins / API auth fails; verify is offline-cacheable via JWKS |
| **Stripe** | Control `billing` | Subscriptions, customer portal, webhooks | Billing state stale; serving unaffected (degradation is generous by design) |
| **NATS JetStream** | All planes (bus) | Cross-plane events/commands | Sync/rebuild/journey stall; serving runs on last-known-good cache |
| **Vault Transit** | Serving/Control (`jwkmanagement`) | Per-store signing keys | Cannot sign/rotate; cached profiles still serve |
| **Vault KV v2** | Control/Sync/Serving (`secretmanagement`) | Store creds, handler config | Connector auth + payment config unavailable |
| **MongoDB** | Control, Sync, Serving read model | Primary datastore | Writes/reads fail per plane; serving cache absorbs some reads |
| **Redis** | Serving | Serving cache + checkout sessions | Serving degraded; memory session fallback exists |
| **Google UCP spec (ucp.dev)** | Serving | The protocol Sarvex's checkout/discovery endpoints conform to | Cannot be "down" — it's a spec, not a service; a store's own conformance is what `activation.verified` checks |
| **Merchant WooCommerce store** | Sync connector, Serving checkout | Source of catalog truth + order write target | Read failure degrades freshness; write failure fails checkout (R1) |

**Key libraries** (`go.mod`): `labstack/echo/v4`, `go.mongodb.org/mongo-driver/v2`, `redis/go-redis/v9`, `ThreeDotsLabs/watermill`, `stripe/stripe-go/v72`, `kinde-oss/kinde-go`, `gorilla/websocket`, `spf13/cobra`+`viper`, `hashicorp/vault/api`, `nats-io/nats.go`. **Frontend** (`package.json`): `vue`, `pinia`, `vue-router`, `axios`, `@kinde-oss/kinde-auth-pkce-js`, `openapi-typescript`, `vite`, `vitest`.

**Contract dependencies:** `sarvex-v1-control-plane.yaml` (OpenAPI 3.0.3 — control-plane HTTP + connector ingest; the console generates its types from it), `sarvex-v1-syncing-plane-async.yaml` (AsyncAPI 2.6.0 — sync-plane event contract). The serving UCP endpoints conform to the **external ucp.dev spec**, not these files.

**Internal build-order dependencies (the dependency chain, not effort order):**

1. **Definitions catalogue + scoring rubric** (`ucp/catalogue`) — blocks scoring, profile rendering, validation.
2. **Plugin + pairing + profile serving** — blocks ingestion and the profile doorway.
3. **Ingestion → mapping → scoring → catalog store** (sync pipeline) — blocks readiness, tasks, journey.
4. **Serving cache + rebuilder + profile rendering + negotiation** — blocks the agent-facing surface.
5. **Checkout** (serving) — depends on the connector (sync) and negotiation; the centerpiece.
6. **Control plane** (identity, journey, tasks, operator, billing) — can proceed alongside the pipeline.
7. **Monitoring** — required before first production go-live.
8. **Validation orchestrator** — the checkout build's test harness; skeleton early.

The **control plane can proceed in parallel** with the sync/serving pipeline; the **definitions catalogue gates** scoring, checkout, and validation.
