Chapter 3 · Specs

CRM Sync — UI Component & ID Registry

  • theme
  • webflow
  • shopify
  • architecture

CRM Sync — UI Component & ID Registry

Canonical naming + delivery model for the storefront UI system (nav, footer, cart, login, search) across design-sync.myshopify.com → crm-sync.dev. One addressable crm- namespace so every surface, collection, and bundle lines up.

Status: working reference. Endpoints marked ✅ are live; ⏳ are planned.


1. Delivery model — three app bundles, loaded from the CF Worker

CRM, PIM, and Design each ship as a separate component bundle served from the Cloudflare Worker's /embed/* endpoints. Updating a bundle = a worker deploy; the storefront never re-pastes code. (Layer 4 of the Higher-Order Stack.)

Bundle Loads Endpoint (worker) Placement
Stack (Layer 1) GA4 (Consent-Mode-v2, id from /stack/config) + UIkit/GSAP/petite-vue per-need /embed/stack-loader.js ✅ helmet (<head>)
CRM <crm-brand-studio>, <crm-brand>, <crm-sync> (event bus) /embed/crm-elements.js ✅ defer
PIM <pim-*> + crmPim.resolve() GID⇄Webflow⇄Xano /embed/pim-elements.js ✅ defer
Design brand theme / tokens (Layer 2 look) /brand/<slug>/theme.css ✅ helmet
Docs modal window.crmDocsModal (docs utility) Pages docs-nav.js (data-modal-only) defer

Bundles are served from the worker, so an update ships with wrangler deploy — the storefront never re-pastes code. The Stack loader is window.crmStack (data-stack="uikit", data-worker, data-shop; GA4 pulled from /stack/config per-shop, MP secret server-side). The Brand Designer loads the SAME loader with data-stack="uikit,petite-vue,gsap".

Placement rule: the Nav loads in the helmet (head — no FOUC, paints first). The Footer loads deferred (defer) at end of body and carries the Shopify / CRM Web Components for Login + Cart.


2. ID registry (the crm- namespace)

Header nav (shopify-uikit-nav.liquid)

Element id class Notes
Nav wrapper / mount — .crm-nav petite-vue mount root
Search #crm-nav-search .crm-nav-search UIkit search icon → routes.search_url
Cart #crm-nav-cart .crm-nav-cart UIkit cart icon + [data-cart-count] badge
Login #crm-nav-login .crm-nav-login → routes.account_login_url
CTA — .crm-nav-cta primary button
Offcanvas (mobile) #crm-nav-offcanvas — UIkit offcanvas
Mobile search / cart / login #crm-nav-search-m · #crm-nav-cart-m · #crm-nav-login-m inside offcanvas
Element id Notes
Footer wrapper #crm-footer render root
Login (web component) #crm-nav-login shared login id — <crm-login> / Shopify Web Component
Cart (web component) #crm-nav-cart shared cart id — <crm-cart>
Footer nav mount #crm-footer-nav from GET /nav?menu=footer

Cart and Login share one id each across nav + footer so a single cart/login component instance binds regardless of which surface triggered it.

Collections (Webflow → sync → worker)

Collection List element id Item / link class Menu key
Nav Menu (header) #crm-nav-collection .crm-nav-item / .crm-nav-link main
Footer Menu #crm-footer-collection .crm-footer-item / .crm-footer-link footer
Tags → Category #crm-category-collection .crm-category-item (category tables)

Collection field slugs (what the sync reads): title, url, active, locale (+ optional order, group).


3. Data endpoints (worker)

Endpoint Method Auth Purpose
/stack/config?shop= GET public GA4 measurement ID (public subset only) ✅
/nav?shop=&menu=&locale= GET public named menu (main/footer), localized ✅
/nav?shop=&menu=&locale= POST admin/tenant write a menu (Webflow sync / config app) ✅
/flow/campaign POST admin/tenant Shopify Flow action → GA4 Smart Bidding (consent-gated, revenue-weighted) ✅
/categories?shop=&locale=&kind= GET/POST public / admin Tags as Category Collection ✅ (kind filter; mirrors /nav)

menu resolution: nav_menus[menu].i18n[locale] → [lang] → .items. Each locale is a separately-editable instance (English + globalized variants).


4. Tags = Category Collection Tables

Tags are modeled as a Category Collection — the same collection→sync→worker pattern as nav, backed by the category tables (Xano channels(201) / channel_membership(202) / category_pivot(203)). A Webflow "Category" collection (#crm-category-collection) syncs to /categories, and category-driven UI (filters, tag chips, audience membership) reads from it — one authoring surface, localized, projected to every surface.


5. Where each piece goes (paste map)

Piece Theme location Loading
Stack loader + UIkit CSS + nav <style> <head> (helmet) blocking CSS, async JS
Nav markup top of body / section server-rendered Liquid
Footer markup + CRM/PIM elements before </body> defer
Brand theme.css (Design) <head> blocking

Long-term: the worker's Theme App Extension app embed injects the helmet + deferred bundles, so there's no manual theme editing.


6. Triggers & Actions → SSR Functions

Four axes are modeled as Triggers whose Actions execute in server-side functions (worker functions / Shopify Functions) — never in theme JS. They ride the consent-aware event bus (Tier A), are fail-closed, and are audit-logged.

Axis Trigger (fires on…) Action (server-side) SSR function / seam
Brand brand/theme selected or published re-theme — emit theme.css / tokens for the surface /brand/<slug>/theme.css · Design bundle
QA / PROD promote between realms (Stage → Prod → Deploy-Live) gate + swap env-scoped config/creds; fail-closed if connections/approvals incomplete /brand/<slug>/promote · env-label config
Persona role / entitlement change (Designer, QA, Release Eng) cap check at the data plane; hide-by-cap in UI, enforce in worker entitlements(190) · hasCap() / userIsDesignerForBrand()
Event consent change · cart · AP2 mandate · A2A delegation run the handler (project to GA4/ESB, gate the cart, settle) consent-gated dataLayer bus → worker/Shopify Functions

The rule: a Trigger is a condition on the bus; its Action is a deterministic server-side function (input-bounded, side-effect-scoped), re-checked per loop. Presentation (nav/footer/components) only reflects the result — it never decides. This is why the same trigger holds whether a human or an agent fired it.

Backs onto: release personas / separation-of-duties, the brand env realms (Stage-Agency / Prod-Agency / Deploy-Live), and entitlement_changes(193) / AP2 agentic_checkout on the event bus.


7. Identity spine — every Shopify GID ⇄ same-name Webflow item

Invariant: every Shopify GID (gid://shopify/Product/…, Collection, Customer, Order, …) has a same-name item in Webflow, joined to a Xano row as the durable source of truth. One entity, three representations, one natural key.

   SHOPIFY gid://…  ⇄  XANO row (SoR, natural key + gid)  ⇄  WEBFLOW item (same name/slug)
                         ▲            sockets (CF Worker)            ▲
                         └──────── AI / CF / Xano orchestration ─────┘

Rules (from the ORM discipline in PROCESS-MANAGEMENT-DATA-LAYER.md):

  • Stable natural key (SKU/handle/name), not a platform id, is the join — the item survives being re-created in any one platform. The GID is carried as an attribute, not the key.
  • Xano is the source of truth; Shopify and Webflow are projections. Data flows out of Xano; no channel is the only place a fact exists.
  • One writer per field (price → Xano, published-state → Webflow, fulfillment → Shopify); versioned upserts, idempotent on (natural_key, source).
  • Sockets = CF Worker owns the mapping + idempotency + conflict resolution; AI orchestrates match/normalize/enrich; Xano persists. Same seam the nav / footer / category collections ride — the collection items are just GID-keyed rows projected to each surface.

This makes every entity agent-addressable and consent-qualified end to end: an AI agent resolves a Shopify GID → Xano row → Webflow item (and back) through one worker socket, under caps + consent, per loop.


8. Semantic wrapper — machine legibility (AEO + a11y)

The UIkit / semantic wrapper is what makes every #crm- id machine-legible. Because uk-* is BEM-namespaced (conflict-free by construction) it supplies the semantic skeleton that utility CSS can't; on top of it we attach a role + attribute pattern keyed to the id so screen readers and answer engines parse the same structure.

Pattern per addressable element:

Layer Carries Example (#crm-nav-cart)
id the address id="crm-nav-cart"
ARIA role / label a11y semantics (WCAG) aria-label="Cart"
data-crm-role machine role for AEO / agents data-crm-role="cart"
data-crm-region landmark on the wrapper <nav … data-crm-region="primary-nav">
UIkit BEM class component structure .uk-navbar-item

Applied on the nav today: data-crm-region="primary-nav" on the <nav> landmark; data-crm-role="search|cart|login" on the controls (+ native role/aria-label). Same pattern extends to footer, category chips, and every <crm-*> component — one wrapper, three readers (browser, screen reader, answer engine / agent).

Scores against the a11y (WCAG) + machine-index (AEO) harness — the wrapper is how we keep both high without hand-tuning each surface.