Chapter 1 · Setup

CRM Sync Setup Reference

The full technical reference for the CRM Sync stack. New users: start with the short numbered guide at crm-sync.dev/start — this page is for depth.

  • xano
  • shopify
  • webflow
  • consent
  • ga4

CRM Sync Setup Reference

Getting started? Follow the step-by-step setup at crm-sync.dev/start — a short numbered list that tells you exactly where each key comes from and which screen it goes into. This page is the full reference, for when you want every detail behind those steps.

Everything you need to get your CRM system running. Complete each section in order — each one builds on the last.

What is CRM Sync? It connects six services together: a server (Cloudflare Worker) that runs your auth and sync logic, a database (Xano) that stores users and tag relationships, a store (Shopify) for customer sync, analytics (Google GA4) for tracking consent and user segments, email (Resend) for transactional emails, and a CMS (Webflow) that displays user data and manages campaigns. You configure each one, then the Webflow App ties them together.


1.4.1 Tab 1: Cloudflare Worker

What is this?

The Cloudflare Worker is your backend server. It handles:

  • User authentication — signup, login, password reset, OAuth (Google + Shopify)
  • Consent management — cookie banners, TOS acceptance, GDPR compliance
  • Customer sync — bidirectional tag sync between Shopify, Xano, Webflow CMS, and GA4
  • Tag system — structured CRM tags with categories (status, tier, segment, campaign, consent, marketing)
  • GA4 integration — pushes user properties and events to Google Analytics via Measurement Protocol
  • Serving UI — the login modal, consent banner, account page, and UCP dashboard are all served from here

Why Cloudflare? It's fast (runs at the edge, close to your users), has a generous free tier, and includes KV storage for caching config and session data.

Setup Steps

Prerequisites

  • Cloudflare account (sign up free)
  • Node.js 20 or newer (download)
  • Wrangler CLI — Cloudflare's command-line tool

Install Wrangler:

npm i -g wrangler

Step 1 — Log in to Cloudflare

wrangler login

This opens a browser window. Log in and authorize Wrangler.

Step 2 — Create a KV Namespace

KV (Key-Value) is a simple storage system. The worker uses it to store session data, tag table IDs, and configuration from the Webflow App.

wrangler kv namespace create CRM_STATE

You'll see output like:

{ binding = "CRM_STATE", id = "abc123..." }

Copy the id value — you'll need it in the next step.

Step 3 — Configure wrangler.toml

Open workers/crm-sync/wrangler.toml and fill in your values:

name = "your-crm-worker"
main = "src/index.ts"
compatibility_date = "2025-04-21"
workers_dev = true

[[kv_namespaces]]
binding = "CRM_STATE"
id = "<paste your KV namespace id here>"

[vars]
XANO_BASE_URL = "https://your-instance.xano.io/api:YOUR_API"
XANO_WORKSPACE_ID = "4"
AUTH_REDIRECT_ORIGIN = "https://your-site.webflow.io"
GOOGLE_CLIENT_ID = ""
SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID = ""
SHOPIFY_SHOP_ID = ""
SHOPIFY_STORE_DOMAIN = "your-store.myshopify.com"
RESEND_FROM_EMAIL = "Your Brand <noreply@yourdomain.com>"

[triggers]
crons = ["*/15 * * * *"]

The cron trigger runs a full customer sync every 15 minutes (Shopify → Xano → Webflow CMS → GA4). Real-time sync also happens via Shopify webhooks and on every user signup/login/tag change.

Don't worry about filling in every field right now. You'll get these values as you complete the other tabs. You can also set them later from the Webflow App.

Step 4 — Set Secrets

Secrets are sensitive values (API keys, tokens) that shouldn't be in your config file. Set each one:

cd workers/crm-sync

wrangler secret put JWT_SECRET
# When prompted, paste a random string. Generate one with: openssl rand -hex 32

wrangler secret put XANO_API_KEY
# Paste your Xano Meta API key (see Tab 2)

wrangler secret put GOOGLE_CLIENT_SECRET
# Paste from Google Cloud Console (see Tab 4)

wrangler secret put SHOPIFY_ADMIN_TOKEN
# Paste your shpua_ token (see Tab 3)

wrangler secret put RESEND_API_KEY
# Paste from resend.com/api-keys

wrangler secret put GA4_API_SECRET
# Paste from GA4 Admin > Data Streams > Measurement Protocol API secrets (see Tab 4)

Step 5 — Deploy

npm install
npx wrangler deploy --config wrangler.toml

Your worker will be live at: https://your-crm-worker.<your-account>.workers.dev

Step 6 — Verify

curl https://your-crm-worker.<your-account>.workers.dev/health

You should see:

{"status":"ok","service":"crm-sync"}

Alternative: Skip the CLI

If you install the CRM Sync Webflow extension, you can set all credentials from the Config tab in the Webflow Designer panel. Values saved there override wrangler.toml — so after the initial deploy, you never need the command line again.


1.4.2 Tab 2: Xano (Database)

What is this?

Xano is your database. It stores all your user accounts, consent records, profile data, and the CRM tag system. The CRM Worker talks to Xano using the Meta API.

What gets stored:

  • User accounts — email, name, password hash, login provider, Shopify/Google IDs
  • Consent preferences — which policies each user accepted, when, and how
  • Consent audit log — a timestamped record of every consent change (required for GDPR)
  • CRM tags — structured tags with name, slug, and category
  • User-tag assignments — a join table linking users to tags with source attribution
  • User extras — a flexible table for any additional data
Setup Steps

Prerequisites

  • Xano account (sign up — free tier works)
  • A workspace created in Xano

Step 1 — Create Your Tables

Create these 6 tables in Xano. The field names must match exactly.

Table: storefront_users

This is your main users table.

Field Type What it stores
id integer Auto-generated unique ID
email text User's email (must be unique)
password_hash text Encrypted password (empty for Google/Shopify users)
full_name text Display name
first_name text First name
last_name text Last name
avatar_url text Profile picture URL
provider text How they signed up: email, google, or shopify
google_sub text Google account ID (auto-filled on Google login)
shopify_customer_gid text Shopify customer ID (auto-filled on sync)
status text active, deleted, or suspended
language_pref text Preferred language: en, es, fr, etc.
tags json Customer tags (synced with Shopify, flat array for backward compat)
number_of_orders integer Order count from Shopify
amount_spent float Total spend from Shopify
email_subscription_status text Shopify email marketing status
sms_subscription_status text Shopify SMS marketing status
country text Country from Shopify default address
last_login_at timestamp Last login time
updated_at timestamp Last update time
Table: user_claims

Stores each user's consent choices and auth provider details.

Field Type What it stores
id integer Auto-generated unique ID
user_id integer Links to the user in storefront_users
consent_tos boolean Accepted Terms of Service?
consent_privacy boolean Accepted Privacy Policy?
consent_cookie boolean Accepted analytics cookies?
consent_marketing boolean Opted into marketing?
consent_version text Which version of your policies (e.g., 1.0)
oidc_provider text OAuth provider: google or shopify
shopify_oidc_sub text Shopify OAuth subject ID
google_sub text Google OAuth subject ID
shopify_customer_access_token text Shopify Customer Account API token
segment_id text A/B test segment label
language text Display language preference
updated_at timestamp Last update time
Table: user_extras

A flexible table for any extra data you want per user. Starts empty — add fields as needed.

Field Type What it stores
id integer Auto-generated unique ID
user_id integer Links to the user in storefront_users
updated_at timestamp Last update time
Table: consent_records

An audit log of every consent change. Required by GDPR.

Field Type What it stores
id integer Auto-generated unique ID
user_id integer Links to the user in storefront_users
consent_type text Which consent: tos, privacy, cookie, or marketing
action text What happened: granted or revoked
method text How it happened: banner, signup, compliance-page, etc.
consent_version text Policy version at the time
consent_id text Groups simultaneous changes
user_agent text Browser info (for audit trail)
ga_session_id text Google Analytics session (for attribution)
timestamp text When the user clicked (client time)
created_at timestamp When the server recorded it
Table: crm_tags

Stores the tag definitions used across Shopify, Webflow CMS, and GA4.

Field Type What it stores
id integer Auto-generated unique ID
name text Display name (e.g., "VIP", "New Campaign")
slug text URL-safe key (e.g., vip, new_campaign)
category text Tag category: status, tier, segment, campaign, consent, marketing
created_at timestamp When the tag was created
Table: user_tag_map

Join table linking users to tags — the core of the CRM tag system.

Field Type What it stores
id integer Auto-generated unique ID
user_id integer Links to storefront_users
tag_id integer Links to crm_tags
assigned_at timestamp When the tag was assigned
source text Where the tag came from: shopify, ucp, admin, system

Shortcut: Instead of creating crm_tags and user_tag_map manually, you can use the admin endpoint after deploying the worker:

curl -X POST https://your-worker.workers.dev/admin/init-tag-system?step=xano

This auto-creates both tables and seeds the 17 default tags.

Step 2 — Get Your API Credentials

  1. Go to your Xano workspace
  2. Navigate to Settings > API Keys
  3. Click Create a Meta API key with full access
  4. Note these values:
Value Where to find it Example
Instance URL Your Xano dashboard URL https://your-instance.xano.io
API path In your API group URL api:1Zsx4CNw
Workspace ID In the browser URL bar 4
API Key Shown after creating eyJhbG... (long string)

Step 3 — Enter in Worker

The full XANO_BASE_URL combines your instance URL and API path:

https://your-instance.xano.io/api:YOUR_API_PATH

Enter this in the Webflow App > Config tab, or in wrangler.toml under [vars].

Step 4 — Verify

Test the connection by creating a test user:

curl -s -X POST https://your-worker.workers.dev/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","password":"testpass123","first_name":"Test","last_name":"User","consent_tos":true,"consent_privacy":true}'

A 201 response with a token means Xano is connected and working.


1.4.3 Tab 3: Shopify (Store Integration)

What is this?

The Shopify integration does four things:

  1. Admin API — Syncs customer data between your CRM and Shopify. Tags, metafields, consent status, and order data are synced in real-time via webhooks and every 15 minutes via cron. Also handles GDPR data requests.

  2. Real-Time Webhooks — When a customer is created or updated in Shopify, webhooks immediately sync to Xano and Webflow CMS. New Shopify customers automatically receive a welcome email to set their website password.

  3. Customer Account OAuth — Lets users sign in with their Shopify account using PKCE (no client secret needed).

  4. CRM Tag Sync — Tags added from the UCP dashboard flow to Shopify as both customer tags (for Shopify Segments/Flows) and structured metafields (for custom reporting).

You need three things from Shopify: a Dev Dashboard app (for Admin API token + app secret), a Headless channel (for OAuth login), and GDPR webhook URLs.

Setup Steps: Dev Dashboard App (Admin API)

The Admin API token lets the worker read and write customer data in your Shopify store. Tokens are obtained via OAuth through the Dev Dashboard (the legacy "Develop apps" flow in Shopify Admin is deprecated).

Step 1 — Create a Dev App

  1. Go to Shopify Dev Dashboard (partners.shopify.com > Apps)
  2. Click Create App
  3. Name it (e.g., "CRM Sync")
  4. Set the App URL to your worker: https://your-worker.workers.dev

Step 2 — Set Permissions

In your shopify.app.crm-sync.toml:

[access_scopes]
scopes = "read_customers,write_customers,customer_read_customers,customer_write_customers,read_products,read_orders"

read_products is required for the product catalog (/commerce/products) and Shop page embed. read_orders is required for order history (/commerce/orders). Shopify silently drops any scopes not declared here.

Step 3 — Configure OAuth Redirect URLs

Add your worker's callback URL:

[auth]
redirect_urls = [
  "https://your-worker.workers.dev/auth/callback"
]

Deploy the app config:

npx shopify app deploy --config=shopify.app.crm-sync.toml

Step 4 — Install the App (Get Token)

  1. Visit https://your-worker.workers.dev/auth/install?shop=your-store.myshopify.com
  2. Approve the OAuth prompt in Shopify
  3. The worker exchanges the code for an expiring shpua_ access token (60-min TTL) + shprt_ refresh token (90-day TTL) and stores both in KV
  4. The worker automatically registers CUSTOMERS_CREATE and CUSTOMERS_UPDATE webhooks for real-time sync

Expiring tokens are mandatory since April 1, 2026 for all Shopify apps. Non-expiring tokens return 403. The worker automatically refreshes the access token before it expires via the */15 * * * * cron. You can also force-refresh from the Settings page ("Force Token Refresh" button) or via POST /admin/shopify-refresh. Shopify rotates the refresh token on each use — the worker handles this automatically.

Step 5 — Set the App Secret

Copy the Client Secret from Dev Dashboard > App > Settings (starts with shpss_).

Enter it in the Webflow App > Config > App Client Secret field, or:

wrangler secret put SHOPIFY_ADMIN_TOKEN

The app secret is used for OAuth code exchange. The shpua_ access token is obtained automatically via the install flow.

Shopify Metafields (Automatic)

When you initialize the tag system (POST /admin/init-tag-system?step=shopify), the worker creates these customer metafield definitions:

Metafield Type What it stores
custom.crm_status Single-line text Active, Inactive, etc.
custom.crm_tier Single-line text VIP, Prospect, etc.
custom.crm_segment Single-line text High Value, At Risk, etc.
custom.crm_tags List (text) All CRM tag slugs
custom.crm_consent_marketing Boolean Marketing consent status
custom.crm_consent_tos Boolean TOS consent status

These are queryable in Shopify Segments: customer.metafield.custom.crm_segment = "high_value".

Setup Steps: "Sign in with Shopify" (OAuth)

This lets your customers log in using their existing Shopify account — no separate password needed.

Step 1 — Set Up Headless Channel

  1. In Shopify Admin, go to Sales channels (left sidebar)
  2. Click Headless (if not installed, add it from the sales channels list)
  3. Click Create storefront or select your existing headless storefront

Step 2 — Get Client ID and Shop ID

  1. On the Headless channel page, under Manage API access, click Manage next to Customer Account API
  2. Copy the Client ID — a UUID like 890afa5e-c87b-496e-...
  3. Copy the Shop ID — a number like 64312475691

Step 3 — Add Redirect URL

If the "Application setup" card is greyed out / read-only (the pencil does nothing): open the Partner dashboard → your store's custom app → API access requests and enable "Allow network access in checkout and account UI extensions." That toggle releases the card for editing. Do not waste time on reinstalling the app or the Dev Dashboard — neither unlocks it.

On the same Customer Account API page, add your Worker's callback URL:

https://your-crm-worker.<account>.workers.dev/auth/shopify/callback

Step 4 — Enter in Worker

Config Field Value
SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_ID The Client ID (UUID)
SHOPIFY_SHOP_ID The numeric Shop ID
SHOPIFY_STORE_DOMAIN your-store.myshopify.com

Set these in the Webflow App > Config tab, or in wrangler.toml.

Setup Steps: GDPR Compliance Webhooks

If you plan to list on the Shopify App Store, you must register GDPR webhook endpoints.

In your shopify.app.crm-sync.toml:

[webhooks]
api_version = "2026-04"

  [[webhooks.subscriptions]]
  uri = "/api/webhooks"
  compliance_topics = [ "customers/data_request", "customers/redact", "shop/redact" ]

The worker has handlers at:

  • POST /gdpr/customer-redact — deletes/anonymizes user data
  • POST /gdpr/data-request — compiles all stored data for a user
  • POST /gdpr/shop-redact — acknowledges shop-level data removal

1.4.4 Tab 4: Google (Analytics, OAuth & GA4 Segments)

What is this?

The Google integration has three parts:

  1. Google OAuth — "Sign in with Google" button via OpenID Connect.

  2. GA4 Consent Mode — Connects your CRM consent banner to Google Analytics. When a user accepts or rejects cookies, GA4 is updated to respect their choice.

  3. GA4 Measurement Protocol — Server-side push of CRM user properties to GA4. Every tag change (from the UCP dashboard, Shopify sync, or admin API) pushes structured user properties to GA4, making them available for GA4 audiences, Google Ads audience sharing, and Looker Studio.

GA4 User Properties pushed by the worker:

Property Source Example Value
crm_status Status category tags active
crm_tier Tier category tags vip
crm_segment Segment category tags high_value,returning
crm_campaign Campaign category tags new_campaign,summer_2026
crm_tags All tag slugs active,vip,new_campaign
consent_marketing Consent tags granted or denied
consent_tos Consent tags granted or denied

Events sent:

  • crm_tags_updated — fired when a user adds/removes tags from the dashboard (includes tags_added, tags_removed, campaign_tags params)
  • crm_sync — fired during the 15-minute cron sync from Shopify (includes source: shopify_cron)
Setup Steps: Google OAuth ("Sign in with Google")

Step 1 — Create OAuth Credentials

  1. Go to Google Cloud Console
  2. Create a project (or select an existing one)
  3. Navigate to APIs & Services > Credentials
  4. Click Create Credentials > OAuth client ID
  5. Application type: Web application
  6. Name it anything (e.g., "CRM Auth")

Step 2 — Set Redirect URI

Under Authorized redirect URIs, add:

https://your-crm-worker.<account>.workers.dev/auth/google/callback

Step 3 — Set JavaScript Origin

Under Authorized JavaScript origins, add your Webflow site:

https://your-site.webflow.io

Step 4 — Copy Credentials

You'll get two values:

  • Client ID: looks like 123456789-xxxx.apps.googleusercontent.com
  • Client Secret: looks like GOCSPX-xxxx

Enter the Client ID in the Webflow App > Config (or wrangler.toml).

Set the Client Secret as a Wrangler secret:

wrangler secret put GOOGLE_CLIENT_SECRET

Step 5 — Enable Google Identity API

In Google Cloud Console > APIs & Services > Library, search for and enable Google Identity.

Setup Steps: GA4 Measurement Protocol (Server-Side Segments)

This sends CRM tag data directly to GA4 as user properties — available for audiences, remarketing, and reporting.

Step 1 — Get Your Measurement ID

  1. Go to Google Analytics
  2. Navigate to Admin > Data Streams > Web
  3. Copy the Measurement ID (looks like G-XXXXXXXXXX)

Step 2 — Create an API Secret

  1. In the same Data Stream, scroll to Measurement Protocol API secrets
  2. Click Create
  3. Name it (e.g., "CRM Sync Server")
  4. Copy the secret value

Step 3 — Enter in Worker

Set both values in the Webflow App > Config > Google Analytics (GA4) section:

  • Measurement ID: G-XXXXXXXXXX
  • Measurement Protocol API Secret: the secret you created

Or via CLI:

wrangler secret put GA4_API_SECRET

And add to wrangler.toml:

GA4_MEASUREMENT_ID = "G-XXXXXXXXXX"

Step 4 — Create User-Scoped Custom Dimensions in GA4

To build audiences from CRM tags:

  1. Go to GA4 Admin > Custom definitions > Create custom dimension
  2. Add these as User-scoped dimensions:
Dimension name User property Scope
CRM Status crm_status User
CRM Tier crm_tier User
CRM Segment crm_segment User
CRM Campaign crm_campaign User
CRM Tags crm_tags User
Marketing Consent consent_marketing User
TOS Consent consent_tos User

Step 5 — Build GA4 Audiences

Once user properties flow in, create audiences:

  1. Go to GA4 Admin > Audiences > New Audience
  2. Examples:
    • VIP Customers: crm_tier contains "vip"
    • Campaign Targets: crm_campaign contains "summer_2026"
    • Marketing Opted-In: consent_marketing equals "granted"
    • At-Risk Segment: crm_segment contains "at_risk"

These audiences automatically sync to Google Ads for remarketing.

Setup Steps: GA4 Consent Mode (Client-Side)

This connects your consent banner to GA4 so tracking respects user choices.

Step 1 — Add GA4 to Your Webflow Site

Go to Webflow Site Settings > Custom Code > Head Code and paste:

<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}

  gtag('consent', 'default', {
    'analytics_storage': 'denied',
    'ad_storage': 'denied',
    'ad_user_data': 'denied',
    'ad_personalization': 'denied',
    'functionality_storage': 'granted',
    'security_storage': 'granted',
    'wait_for_update': 500
  });

  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

Replace G-XXXXXXXXXX with your Measurement ID.

This must go before the CRM Footer Code so that gtag is defined when the consent banner loads.

Step 2 — Add the Consent Bridge Script

In Webflow Site Settings > Custom Code > Footer Code, paste this after the CRM Footer embed:

<!-- CRM Consent > GA4 Consent Mode bridge -->
<script>
(function() {
  function updateGa4Consent(flags) {
    if (typeof gtag !== 'function') return;

    gtag('consent', 'update', {
      'analytics_storage': flags.cookie ? 'granted' : 'denied',
      'ad_storage': flags.marketing ? 'granted' : 'denied',
      'ad_user_data': flags.marketing ? 'granted' : 'denied',
      'ad_personalization': flags.marketing ? 'granted' : 'denied'
    });

    gtag('event', 'consent_update', {
      'consent_tos': flags.tos,
      'consent_privacy': flags.privacy,
      'consent_cookie': flags.cookie,
      'consent_marketing': flags.marketing,
      'consent_method': 'banner'
    });
  }

  var stored = window._crmConsent && window._crmConsent.getConsent();
  if (stored) updateGa4Consent(stored);

  if (window._crmConsent) {
    var origSetItem = localStorage.setItem.bind(localStorage);
    localStorage.setItem = function(key, val) {
      origSetItem(key, val);
      if (key === 'crm_consent') {
        try { updateGa4Consent(JSON.parse(val)); } catch(e) {}
      }
    };
  }
})();
</script>

Step 3 — (Optional) Event-Scoped Custom Dimensions

For consent event reporting in GA4:

Dimension name Event parameter Scope
Consent TOS consent_tos Event
Consent Privacy consent_privacy Event
Consent Cookie consent_cookie Event
Consent Marketing consent_marketing Event
Consent Method consent_method Event
Google Tag Manager — not required

You do not need a GTM container to measure anything. GA4 is delivered via gtag in the CRM Sync stack loader (/embed/stack-loader.js), with Consent Mode v2 defaults applied before any tag fires.

GTM is optional and additive: if you already maintain your own marketing tags in a container, paste its GTM- ID into Connect Google → Tag Manager container and CRM Sync loads that container behind the same consent gate as everything else. Point the container at your own tags — not at the GA4 property you connected above, or that property will count every hit twice.

This section previously walked through creating a container and pasting the snippet into Webflow. That was written for the OMEN/Webflow-era build and did not describe how the app actually ships tags, so it has been removed rather than left to contradict the product.

If you run your own GTM container for reasons of your own, it can coexist — just don't expect CRM Sync to populate it.

Setting up for the first time? The canonical, step-by-step new-user guide — including where every key comes from — is crm-sync.dev/start, with the key reference at crm-sync.dev/start#keys. It is maintained against the running worker, so it stays correct as the app changes.

CRM Form Bridge: E2E Event Tracking (Any Form Type)

The CRM Footer embed includes a generic form bridge that auto-tags, logs consent, and fires GA4 events for any form — newsletter, waitlist, demo request, contact, quiz, etc. No extra scripts needed.

How It Works

Add a data-crm-form attribute to any Webflow form. The attribute value becomes the tag name:

<!-- Newsletter -->
<form data-crm-form="newsletter">
  <input type="email" name="email" placeholder="you@example.com" />
  <button type="submit">Subscribe</button>
</form>

<!-- Waitlist -->
<form data-crm-form="waitlist">
  <input type="email" name="email" />
  <button type="submit">Join Waitlist</button>
</form>

<!-- Demo Request -->
<form data-crm-form="demo_request">
  <input type="email" name="email" />
  <button type="submit">Book Demo</button>
</form>

In Webflow Designer: select the form block → Settings panel → Custom Attributes → add data-crm-form with the form type as the value.

Data Flow

User submits form (data-crm-form="waitlist")
  → dataLayer.push({ event: 'crm_form_submit', form_type: 'waitlist', ... })
  → GTM fires GA4 event tag
  → POST /ucp/tags (adds 'waitlist_subscribed' + 'waitlist_2026-05-14' tags)
  → POST /auth/consent-sync (logs waitlist + marketing consent with GA4 session)
  → Worker channel flow:
      1. Xano crm_tags — auto-creates tag (category: campaign)
      2. Xano user_tag_map — join entry (source: ucp)
      3. Shopify — tagsAdd + metafields
      4. Webflow CMS — Tags collection item (if new)
      5. GA4 — user properties + crm_tags_updated event
  → consent_records — audit entry with session ID + timestamp

What Gets Created Per Form Type

Form Attribute Tags Created Consent Logged Shopify Tag
data-crm-form="newsletter" newsletter_subscribed, newsletter_2026-05-14 newsletter: granted accepts_newsletter
data-crm-form="waitlist" waitlist_subscribed, waitlist_2026-05-14 waitlist: granted accepts_waitlist
data-crm-form="demo_request" demo_request_subscribed, demo_request_2026-05-14 demo_request: granted accepts_demo_request
data-crm-form="contact" contact_subscribed, contact_2026-05-14 contact: granted accepts_contact

The date-stamped tag gives you temporal segmentation — see which campaign day drove signups.

GTM Tag Setup

In GTM, create one tag for all form types:

Tag: GA4 — CRM Form Submit
Setting Value
Tag Type GA4 Event
Event Name crm_form_submit
Event Parameters form_type → {{DLV - form_type}}, email → {{DLV - email}}, session_id → {{DLV - session_id}}, submit_timestamp → {{DLV - submit_timestamp}}, consent_marketing → {{DLV - consent_marketing}}, crm_user_id → {{DLV - crm_user_id}}, source_page → {{Page Path}}
Trigger Custom Event: crm_form_submit
Data Layer Variables
Variable Name Data Layer Variable Name
DLV - form_type form_type
DLV - email email
DLV - session_id session_id
DLV - submit_timestamp submit_timestamp
DLV - consent_marketing consent_marketing
DLV - crm_user_id crm_user_id

GA4 Custom Dimensions

In GA4 Admin, add these event-scoped custom dimensions:

Dimension name Event parameter Scope
Form Type form_type Event
Form Email email Event
GA4 Session ID session_id Event
Submit Timestamp submit_timestamp Event
Source Page source_page Event

Build GA4 Audiences

Examples:

Audience Condition
Newsletter Subscribers crm_tags contains "newsletter_subscribed"
Waitlist Signups crm_tags contains "waitlist_subscribed"
Demo Requests Event: crm_form_submit where form_type = "demo_request"
All Form Submitters Event: crm_form_submit (any type)

These audiences auto-sync to Google Ads for remarketing.

Dashboard Visibility

After form submission, the user's UCP Dashboard shows:

Card What appears
Consent Status "Newsletter: Granted" (or whichever form type maps to a known consent column)
Consent History Timestamped row: waitlist — granted — waitlist_form — 5/14/2026
Customer Tags newsletter_subscribed, waitlist_subscribed, date tags
Retarget Channels Email, SMS, Ads, Push light up (marketing consent granted)
A/B Segment Campaign tags shown under "GA4 Synced"

Manual / Programmatic Use

For non-Webflow forms or custom integrations:

// Submit programmatically
window._crmForms.submit('newsletter', 'user@example.com', formElement);
window._crmForms.submit('waitlist', 'user@example.com');
window._crmForms.submit('demo_request', 'user@example.com', document.getElementById('my-form'));

Verify E2E

  1. Open your Webflow site with a data-crm-form form
  2. Open Chrome DevTools > Network tab
  3. Submit the form with a test email
  4. Check:
    • Console: dataLayer.filter(e => e.event === 'crm_form_submit') — shows form_type, email, session_id
    • Network: POST /ucp/tags with {form_type}_subscribed tag
    • Network: POST /auth/consent-sync with method: {form_type}_form
    • GTM Preview: crm_form_submit trigger fires
    • GA4 DebugView: crm_form_submit event with all parameters
    • Shopify Admin > Customers: user has accepts_{form_type} tag

Session Continuity

The GA4 session ID (_ga_ cookie) is captured at submit time and attached to:

  • The GTM dataLayer event (client-side)
  • The consent_records audit entry (server-side, stored in Xano)
  • The GA4 Measurement Protocol push (server-side)

This lets you join client-side GA4 sessions with server-side CRM events in BigQuery for full journey analysis.


1.4.5 Tab 5: Webflow CMS (Customer Data & Tags)

What is this?

Webflow CMS stores a read-friendly copy of your customer data and CRM tags. This lets you build Webflow pages that display customer profiles, filter by tags, and create dynamic content based on CRM segments.

Two collections are used:

  1. Customers — synced from Xano on every cron run (name, email, provider, consent status, order data, tags)
  2. CRM Tags — the tag definitions with categories, referenced by the Customers collection via MultiReference
Setup Steps

Step 1 — Get a Webflow CMS API Token

  1. Go to Webflow Site Settings > Integrations > API Access
  2. Click Generate API Token
  3. Required scopes: CMS read/write
  4. Copy the token

Step 2 — Create the Customers Collection

Create a CMS collection called "Customers" with these fields:

Field Type Slug
Name Plain Text name
Email Email email
First Name Plain Text first-name
Last Name Plain Text last-name
Provider Plain Text provider
Status Plain Text status
Language Plain Text language
Tags Plain Text tags
Number of Orders Number number-of-orders
Amount Spent Number amount-spent
Consent TOS Plain Text consent-tos
Consent Privacy Plain Text consent-privacy
Consent Cookie Plain Text consent-cookie
Consent Marketing Plain Text consent-marketing
Email Subscription Plain Text email-subscription
SMS Subscription Plain Text sms-subscription
Shopify Customer ID Plain Text shopify-customer-id
Country Plain Text country
Tag Refs Multi-Reference → CRM Tags tag-refs

Copy the Collection ID from the collection settings.

Step 3 — Initialize Tag System (Creates CRM Tags Collection)

curl -X POST https://your-worker.workers.dev/admin/init-tag-system?step=webflow

This creates the CRM Tags collection and populates it with the 17 default tags.

Step 4 — Enter in Worker

Set in the Webflow App > Config tab:

  • CMS API Token: the token from Step 1
  • Customers Collection ID: from Step 2

Campaign Tags from Dashboard

When a user adds a tag like "new campaign" from the UCP dashboard, it immediately:

  1. Creates the tag in Xano (crm_tags table, category: campaign)
  2. Assigns it to the user in the join table (user_tag_map)
  3. Pushes it to Shopify as a customer tag + metafield
  4. Creates it in the Webflow CRM Tags collection
  5. Pushes it to GA4 as a crm_campaign user property

No cron wait — the full channel flow happens in one request.


1.4.6 Tab 6: Email (Resend)

What is this?

Resend handles transactional emails:

  1. Password reset — When a user clicks "Forgot password?", the worker sends a reset link (1-hour expiry).
  2. Welcome email — When a customer is added in Shopify and synced to the CRM, they automatically receive a "Welcome — set up your password" email (24-hour expiry). This lets Shopify-origin users create a password to sign in on the website. The forgot-password flow also detects first-time users and sends the welcome variant instead of the reset variant.
Setup Steps

Step 1 — Create a Resend Account

  1. Go to resend.com and sign up
  2. Verify your sending domain

You must verify the domain in Resend before you can send from it.

Step 2 — Get Your API Key

  1. Go to resend.com/api-keys
  2. Create a new API key
  3. Copy it — it starts with re_

Step 3 — Enter in Worker

Set the API key:

wrangler secret put RESEND_API_KEY

Set the "from" email in wrangler.toml or the Webflow App > Config:

RESEND_FROM_EMAIL = "Your Brand <noreply@yourdomain.com>"

The format is: Display Name <email@verified-domain.com>


1.4.7 Tab 7: Webflow Extension (Config UI)

What is this?

The CRM Sync Webflow extension adds a configuration panel to the Webflow Designer. It lets you manage all credentials, auth settings, consent toggles, and embed codes without touching the CLI.

Config tab — Worker URL, Shopify credentials, Google OAuth, Xano, Resend, Webflow CMS, GA4 Auth tab — Toggle auth methods (email/Google/Shopify), session settings, consent & privacy toggles Embeds tab — Copy-paste embed codes for footer loader, account page, dashboard, compliance page Status tab — Health check, endpoint testing, redirect URIs, privacy API tests, GDPR handler tests

Embed Codes

The extension generates four embed snippets:

Embed Where to paste What it renders
CRM Footer Loader Site Settings > Footer Code Login modal, consent banner, session management
Account Page Account page embed block User profile view/edit
UCP Dashboard Dashboard page embed block Consent status, retarget channels, A/B segment, tags, translation, consent history
Compliance / Privacy Privacy page embed block Communication preferences, third-party disclosures, data rights

What is this?

The value-prop layer: Agentic Commerce Checkout (who/which agent may transact, and under what consent) plus optional per-Org enterprise data-layer / ERP add-ons (Adobe AEP, SAP S/4HANA, NielsenIQ/Circana). It is operated entirely from the /settings console — no code changes, no deploy. All config saves to KV and takes effect immediately.

The console is double-gated: it sits behind Cloudflare Access (your zero-trust login) and the admin key. Open it as https://<worker>/settings?key=<ADMIN_KEY> after authenticating with Access.

One-time setup

  1. Scoped read key — mint an entitlement:read key in Xano and provide it as XANO_ENTITLEMENT_KEY. You can paste it into the console (Xano section → Entitlement Read Key → Set, masked) or set it as a worker secret. The high-volume entitlement read path uses this scoped key, never the master metadata key.
  2. Signing key — in the console's Agentic Commerce Core card, click Generate to create the EdDSA signing key (channels verify entitlement tokens offline with its public half). Use Rotate later for a zero-downtime, non-destructive roll.
  3. Channels — the Core card shows each channel's cadence (real-time vs batch), connector status, cursor, and last-synced, with a Sync button. Cloudflare/Shopify/GA4 are real-time (project on every change); batch channels reconcile every 15 minutes.
  • An entitlement is one record per subject (tenant / user / agent) with plan_tier, features[], caps (a2a / ap2 / mandate_max_amount / channels), and an explicit consent object. It carries a monotonic state_version.
  • Consent never regresses: every channel only moves a subject forward in version, so a slow 15-minute batch system can never overwrite a newer consent the real-time channels already applied. Revoking an entitlement drops every channel's projection and denies checkout offline.
  • Consent is projected identically everywhere — granular GA4 Consent Mode v2, Shopify metafields, and the add-on connectors.

Enterprise add-ons (per Org)

In the Enterprise Add-ons card, configure each integration the Org has entitled (an add-on is active only when its feature is in the entitlement's features[]):

Add-on Feature key Point the connector at
Adobe AEP adobe_aep your AEP HTTP streaming endpoint
SAP S/4HANA sap your OData / integration-middleware endpoint
NielsenIQ / Circana nielsen your measurement ingestion endpoint

Each connector is { url, key, enabled }; the worker POSTs the versioned entitlement+consent record there. Onboarding a new Org or stack (Salesforce, Braze, Attentive, …) is configuration, not code.

The console's Header / Footer Embed Codes card provides copy-paste snippets: the Head snippet sets GA4 Consent Mode defaults; the Footer snippet loads the enabled embeds. Paste into Webflow → Site Settings → Custom Code.

Deploy channels & deploy teams

The portal is self-identifying per deploy channel, resolved by the hostname you open it on — one worker, three channels:

Channel Hostname pattern Default deploy team
Dev localhost / *dev* Engineering
Stage *.workers.dev Agency Consulting Team
Prod / UAT production custom domain (crm.story-story.ai) QA · Product Management / DPO · PMO · Deploy On-Prem

The environment·team banner shows at the top of /settings and /setup. Rename a channel's team in the Environment / Deploy Team card (saved per hostname). These values can later be owned by the Webflow Publish Refactor (the publish target is the environment). The Webflow extension header shows the same chip + a Config Portal ↗ link.

Localization (markets & geos)

Each tenant has a market, grouped by geo for scale:

Geo Markets (locale · currency)
NA US en-US·USD · CA en-CA·CAD
EMEA UK en-GB·GBP · DE de-DE·EUR · FR fr-FR·EUR
APAC AU en-AU·AUD · NZ en-NZ·NZD · SG en-SG·SGD · JP ja-JP·JPY
LATAM MX es-MX·MXN · BR pt-BR·BRL

The market is inferred from the shop name (prefix or suffix: us-acme, acme-au) or a country TLD, and overridable in the Localization (Market) card (geo-grouped selector). It sets the locale and the default currency for agent mandates/caps (e.g. an AU tenant defaults to AUD). Add more markets under any geo without code changes.

Forward-Deploy Harness

Changes move forward through the three channels, with each stakeholder operating its own channel from the portal — no code access required downstream:

Dev (Engineering) ──► Stage (Agency Consulting) ──► Prod / UAT (QA · Product Mgmt / DPO · PMO · On-Prem)
   build + verify         configure + UAT prep            sign-off + production operation
  • Each channel binds its own Webflow (UI) and Xano (Data) state targets — both shown in the /settings env banner (a deploy reaches state in both systems).
  • Each channel is operated via /settings (KV config, effective on save — no deploy) and gated per stakeholder by Cloudflare Access (zero-trust) layered over the admin key.
  • Validate any channel with the verification script (use --read-only on Stage/Prod to avoid mutations); /setup shows Agentic Commerce readiness with a Config Portal deep-link.

Verify

Run scripts/verify-entitlement.sh with the admin key to exercise the whole stack end-to-end (entitlement CRUD + state machine, sign/verify/tamper, rotation, channel sync/replay, revoke cascade) with PASS/FAIL output. On Stage/Prod use --read-only — it performs no mutations (only reads + a non-mutating verify against the seeded entitlement).

Clean Room note: the privacy-preserving Clean Room (see CLEAN-ROOM-SPEC.md) is a consumer of this consent — it gates match-job inclusion on consent.clean_room at the current version. It is a separate concern (data matching → aggregates), not part of consent propagation.


1.4.9 Pricing Tiers & Upgrade Exceptions

Shared Plan ($90 one-time download, from $69) — Stakeholder B

Multi-tenant. You use the hosted CRM Sync worker. API keys are entered via the Webflow extension Config tab and stored in the shared worker's KV (isolated per shop). The Plan tab shows your current plan and active features. Custom Worker Setup is locked.

Billing is via Shopify App Billing — charges appear on your Shopify invoice.

Private Worker Plan ($325/mo or custom license) — Stakeholder C

You deploy your own Cloudflare Worker instance. The following UI elements behave differently:

  • Custom Worker Setup: Unlocked. You enter your own Worker URL and CDN domain.
  • Plan tab: Informational only. You already have full access to all features.
  • "Upgrade to unlock" prompts: Hidden. All integrations (Adobe AEP, custom OAuth domains) are self-managed.
  • Subscription billing: Not through Shopify App Billing. Billed separately per license agreement.

Fee responsibility on Private Worker plans:

Cost Who pays
CRM Sync license You (per agreement with App Creator)
Cloudflare Workers + KV Your Cloudflare account
Xano database Your Xano account
Shopify API access Your Shopify Partner/app account
Resend transactional email Your Resend account
Google OAuth + GA4 Your Google Cloud project
Adobe AEP (if enabled) Your Adobe contract

To activate Private plan features: Set plan to "private" in your tenant config:

curl -X POST https://YOUR-WORKER.workers.dev/config?shop=YOUR-STORE.myshopify.com \
  -H "Authorization: Bearer YOUR_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"plan":"private"}'

Self-Service Admin Key Rotation (Persona C)

Private Worker operators can rotate their admin key via API without needing the Cloudflare CLI. This is independent of any Shopify instance — useful when managing multiple Shopify stores from one worker.

Key hierarchy:

  • Root key (ADMIN_KEY): Set once via wrangler secret put at deploy time. Cannot be rotated via API.
  • Rotatable key: Created and managed via /admin/rotate-key. Works alongside the root key on all admin endpoints.

Create or rotate a key:

# Auto-generate a new key
curl -X POST https://YOUR-WORKER.workers.dev/admin/rotate-key \
  -H "Authorization: Bearer YOUR_CURRENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

# Or bring your own key (minimum 20 characters)
curl -X POST https://YOUR-WORKER.workers.dev/admin/rotate-key \
  -H "Authorization: Bearer YOUR_CURRENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"your-custom-key-at-least-20-chars"}'

Check current key status:

curl https://YOUR-WORKER.workers.dev/admin/rotate-key \
  -H "Authorization: Bearer YOUR_KEY"
# Returns: has_rotatable_key, created_at, rotated_at, rotations, key_preview

Revoke rotatable key (root key only):

curl -X DELETE https://YOUR-WORKER.workers.dev/admin/rotate-key \
  -H "Authorization: Bearer YOUR_ROOT_ADMIN_KEY"

Important: Rotating the key immediately invalidates the previous rotatable key. The root ADMIN_KEY always remains valid. Existing tenant tokens (crm_t_*) are not affected by key rotation.


1.4.10 Quick Reference

All credentials in one place. Use the Webflow App Config tab or wrangler.toml / wrangler secret put.

What Where to set Example
Worker URL Webflow App: Config tab https://your-worker.workers.dev
Xano API Base URL Webflow App or wrangler.toml https://xxx.xano.io/api:XXX
Xano API Key Webflow App or wrangler secret eyJhbG...
Google Client ID Webflow App or wrangler.toml 123456.apps.googleusercontent.com
Google Client Secret Webflow App or wrangler secret GOCSPX-xxx
GA4 Measurement ID Webflow App or wrangler.toml G-XXXXXXXXXX
GA4 API Secret Webflow App or wrangler secret xxxxxxxx
Shopify Store Domain Webflow App or wrangler.toml store.myshopify.com
Shopify Shop ID Webflow App or wrangler.toml 64312475691
Shopify Client ID Webflow App or wrangler.toml 890afa5e-...
Shopify Admin Token Webflow App or OAuth install flow shpua_xxx (auto via install)
Shopify App Secret Webflow App or wrangler secret shpss_xxx
Webflow CMS Token Webflow App or wrangler secret xxx...
Webflow Collection ID Webflow App xxx...
Resend API Key Webflow App or wrangler secret re_xxx
Resend From Email Webflow App or wrangler.toml Brand <noreply@domain.com>
JWT Secret wrangler secret only openssl rand -hex 32

1.4.11 Default CRM Tags

These 17 tags are seeded when you initialize the tag system:

Category Tags
status Active, Inactive
tier VIP, Prospect
consent Accepts Marketing, Rejects Marketing, Accepts TOU, Accepts Privacy, Accepts Cookie
marketing Email Subscribed, Email Unsubscribed, SMS Subscribed
segment High Value, Returning, New Customer, At Risk
campaign Campaign

Tags added from the UCP dashboard that don't match a known category default to campaign.