Reference

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.


Tab 1: Cloudflare Worker

What is this?

The Cloudflare Worker is your backend server. It handles:

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.

<details> <summary><strong>Setup Steps</strong></summary>

Prerequisites

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"}

</details>

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.


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:

<details> <summary><strong>Setup Steps</strong></summary>

Prerequisites

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.

FieldTypeWhat it stores
idintegerAuto-generated unique ID
emailtextUser's email (must be unique)
password_hashtextEncrypted password (empty for Google/Shopify users)
full_nametextDisplay name
first_nametextFirst name
last_nametextLast name
avatar_urltextProfile picture URL
providertextHow they signed up: email, google, or shopify
google_subtextGoogle account ID (auto-filled on Google login)
shopify_customer_gidtextShopify customer ID (auto-filled on sync)
statustextactive, deleted, or suspended
language_preftextPreferred language: en, es, fr, etc.
tagsjsonCustomer tags (synced with Shopify, flat array for backward compat)
number_of_ordersintegerOrder count from Shopify
amount_spentfloatTotal spend from Shopify
email_subscription_statustextShopify email marketing status
sms_subscription_statustextShopify SMS marketing status
countrytextCountry from Shopify default address
last_login_attimestampLast login time
updated_attimestampLast update time
Table: user_claims

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

FieldTypeWhat it stores
idintegerAuto-generated unique ID
user_idintegerLinks to the user in storefront_users
consent_tosbooleanAccepted Terms of Service?
consent_privacybooleanAccepted Privacy Policy?
consent_cookiebooleanAccepted analytics cookies?
consent_marketingbooleanOpted into marketing?
consent_versiontextWhich version of your policies (e.g., 1.0)
oidc_providertextOAuth provider: google or shopify
shopify_oidc_subtextShopify OAuth subject ID
google_subtextGoogle OAuth subject ID
shopify_customer_access_tokentextShopify Customer Account API token
segment_idtextA/B test segment label
languagetextDisplay language preference
updated_attimestampLast update time
Table: user_extras

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

FieldTypeWhat it stores
idintegerAuto-generated unique ID
user_idintegerLinks to the user in storefront_users
updated_attimestampLast update time
Table: consent_records

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

FieldTypeWhat it stores
idintegerAuto-generated unique ID
user_idintegerLinks to the user in storefront_users
consent_typetextWhich consent: tos, privacy, cookie, or marketing
actiontextWhat happened: granted or revoked
methodtextHow it happened: banner, signup, compliance-page, etc.
consent_versiontextPolicy version at the time
consent_idtextGroups simultaneous changes
user_agenttextBrowser info (for audit trail)
ga_session_idtextGoogle Analytics session (for attribution)
timestamptextWhen the user clicked (client time)
created_attimestampWhen the server recorded it
Table: crm_tags

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

FieldTypeWhat it stores
idintegerAuto-generated unique ID
nametextDisplay name (e.g., "VIP", "New Campaign")
slugtextURL-safe key (e.g., vip, new_campaign)
categorytextTag category: status, tier, segment, campaign, consent, marketing
created_attimestampWhen the tag was created
Table: user_tag_map

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

FieldTypeWhat it stores
idintegerAuto-generated unique ID
user_idintegerLinks to storefront_users
tag_idintegerLinks to crm_tags
assigned_attimestampWhen the tag was assigned
sourcetextWhere 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:
ValueWhere to find itExample
Instance URLYour Xano dashboard URLhttps://your-instance.xano.io
API pathIn your API group URLapi:1Zsx4CNw
Workspace IDIn the browser URL bar4
API KeyShown after creatingeyJhbG... (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.

</details>


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.
  1. 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.
  1. Customer Account OAuth — Lets users sign in with their Shopify account using PKCE (no client secret needed).
  1. 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.

<details> <summary><strong>Setup Steps: Dev Dashboard App (Admin API)</strong></summary>

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:

MetafieldTypeWhat it stores
custom.crm_statusSingle-line textActive, Inactive, etc.
custom.crm_tierSingle-line textVIP, Prospect, etc.
custom.crm_segmentSingle-line textHigh Value, At Risk, etc.
custom.crm_tagsList (text)All CRM tag slugs
custom.crm_consent_marketingBooleanMarketing consent status
custom.crm_consent_tosBooleanTOS consent status

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

</details>

<details> <summary><strong>Setup Steps: "Sign in with Shopify" (OAuth)</strong></summary>

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 FieldValue
SHOPIFY_CUSTOMER_ACCOUNT_CLIENT_IDThe Client ID (UUID)
SHOPIFY_SHOP_IDThe numeric Shop ID
SHOPIFY_STORE_DOMAINyour-store.myshopify.com

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

</details>

<details> <summary><strong>Setup Steps: GDPR Compliance Webhooks</strong></summary>

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:

</details>


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.
  1. 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.
  1. 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:

PropertySourceExample Value
crm_statusStatus category tagsactive
crm_tierTier category tagsvip
crm_segmentSegment category tagshigh_value,returning
crm_campaignCampaign category tagsnew_campaign,summer_2026
crm_tagsAll tag slugsactive,vip,new_campaign
consent_marketingConsent tagsgranted or denied
consent_tosConsent tagsgranted or denied

Events sent:

<details> <summary><strong>Setup Steps: Google OAuth ("Sign in with Google")</strong></summary>

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:

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.

</details>

<details> <summary><strong>Setup Steps: GA4 Measurement Protocol (Server-Side Segments)</strong></summary>

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:

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 nameUser propertyScope
CRM Statuscrm_statusUser
CRM Tiercrm_tierUser
CRM Segmentcrm_segmentUser
CRM Campaigncrm_campaignUser
CRM Tagscrm_tagsUser
Marketing Consentconsent_marketingUser
TOS Consentconsent_tosUser

Step 5 — Build GA4 Audiences

Once user properties flow in, create audiences:

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

These audiences automatically sync to Google Ads for remarketing.

</details>

<details> <summary><strong>Setup Steps: GA4 Consent Mode (Client-Side)</strong></summary>

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.

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 nameEvent parameterScope
Consent TOSconsent_tosEvent
Consent Privacyconsent_privacyEvent
Consent Cookieconsent_cookieEvent
Consent Marketingconsent_marketingEvent
Consent Methodconsent_methodEvent

</details>

<details> <summary><strong>Google Tag Manager — not required</strong></summary>

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.

</details>

<details> <summary><strong>CRM Form Bridge: E2E Event Tracking (Any Form Type)</strong></summary>

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 AttributeTags CreatedConsent LoggedShopify Tag
data-crm-form="newsletter"newsletter_subscribed, newsletter_2026-05-14newsletter: grantedaccepts_newsletter
data-crm-form="waitlist"waitlist_subscribed, waitlist_2026-05-14waitlist: grantedaccepts_waitlist
data-crm-form="demo_request"demo_request_subscribed, demo_request_2026-05-14demo_request: grantedaccepts_demo_request
data-crm-form="contact"contact_subscribed, contact_2026-05-14contact: grantedaccepts_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
SettingValue
Tag TypeGA4 Event
Event Namecrm_form_submit
Event Parametersform_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}}
TriggerCustom Event: crm_form_submit
Data Layer Variables
Variable NameData Layer Variable Name
DLV - form_typeform_type
DLV - emailemail
DLV - session_idsession_id
DLV - submit_timestampsubmit_timestamp
DLV - consent_marketingconsent_marketing
DLV - crm_user_idcrm_user_id

GA4 Custom Dimensions

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

Dimension nameEvent parameterScope
Form Typeform_typeEvent
Form EmailemailEvent
GA4 Session IDsession_idEvent
Submit Timestampsubmit_timestampEvent
Source Pagesource_pageEvent

Build GA4 Audiences

Examples:

AudienceCondition
Newsletter Subscriberscrm_tags contains "newsletter_subscribed"
Waitlist Signupscrm_tags contains "waitlist_subscribed"
Demo RequestsEvent: crm_form_submit where form_type = "demo_request"
All Form SubmittersEvent: 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:

CardWhat appears
Consent Status"Newsletter: Granted" (or whichever form type maps to a known consent column)
Consent HistoryTimestamped row: waitlist — granted — waitlist_form — 5/14/2026
Customer Tagsnewsletter_subscribed, waitlist_subscribed, date tags
Retarget ChannelsEmail, SMS, Ads, Push light up (marketing consent granted)
A/B SegmentCampaign 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:
  5. Console: dataLayer.filter(e => e.event === 'crm_form_submit') — shows form_type, email, session_id
  6. Network: POST /ucp/tags with {form_type}_subscribed tag
  7. Network: POST /auth/consent-sync with method: {form_type}_form
  8. GTM Preview: crm_form_submit trigger fires
  9. GA4 DebugView: crm_form_submit event with all parameters
  10. 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:

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

</details>


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

<details> <summary><strong>Setup Steps</strong></summary>

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:

FieldTypeSlug
NamePlain Textname
EmailEmailemail
First NamePlain Textfirst-name
Last NamePlain Textlast-name
ProviderPlain Textprovider
StatusPlain Textstatus
LanguagePlain Textlanguage
TagsPlain Texttags
Number of OrdersNumbernumber-of-orders
Amount SpentNumberamount-spent
Consent TOSPlain Textconsent-tos
Consent PrivacyPlain Textconsent-privacy
Consent CookiePlain Textconsent-cookie
Consent MarketingPlain Textconsent-marketing
Email SubscriptionPlain Textemail-subscription
SMS SubscriptionPlain Textsms-subscription
Shopify Customer IDPlain Textshopify-customer-id
CountryPlain Textcountry
Tag RefsMulti-Reference → CRM Tagstag-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:

</details>

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.


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.

<details> <summary><strong>Setup Steps</strong></summary>

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>

</details>


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:

EmbedWhere to pasteWhat it renders
CRM Footer LoaderSite Settings > Footer CodeLogin modal, consent banner, session management
Account PageAccount page embed blockUser profile view/edit
UCP DashboardDashboard page embed blockConsent status, retarget channels, A/B segment, tags, translation, consent history
Compliance / PrivacyPrivacy page embed blockCommunication 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.

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-onFeature keyPoint the connector at
Adobe AEPadobe_aepyour AEP HTTP streaming endpoint
SAP S/4HANAsapyour OData / integration-middleware endpoint
NielsenIQ / Circananielsenyour 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:

ChannelHostname patternDefault deploy team
Devlocalhost / *dev*Engineering
Stage*.workers.devAgency Consulting Team
Prod / UATproduction 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:

GeoMarkets (locale · currency)
NAUS en-US·USD · CA en-CA·CAD
EMEAUK en-GB·GBP · DE de-DE·EUR · FR fr-FR·EUR
APACAU en-AU·AUD · NZ en-NZ·NZD · SG en-SG·SGD · JP ja-JP·JPY
LATAMMX 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

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.


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:

Fee responsibility on Private Worker plans:

CostWho pays
CRM Sync licenseYou (per agreement with App Creator)
Cloudflare Workers + KVYour Cloudflare account
Xano databaseYour Xano account
Shopify API accessYour Shopify Partner/app account
Resend transactional emailYour Resend account
Google OAuth + GA4Your 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:

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.


Quick Reference

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

WhatWhere to setExample
Worker URLWebflow App: Config tabhttps://your-worker.workers.dev
Xano API Base URLWebflow App or wrangler.tomlhttps://xxx.xano.io/api:XXX
Xano API KeyWebflow App or wrangler secreteyJhbG...
Google Client IDWebflow App or wrangler.toml123456.apps.googleusercontent.com
Google Client SecretWebflow App or wrangler secretGOCSPX-xxx
GA4 Measurement IDWebflow App or wrangler.tomlG-XXXXXXXXXX
GA4 API SecretWebflow App or wrangler secretxxxxxxxx
Shopify Store DomainWebflow App or wrangler.tomlstore.myshopify.com
Shopify Shop IDWebflow App or wrangler.toml64312475691
Shopify Client IDWebflow App or wrangler.toml890afa5e-...
Shopify Admin TokenWebflow App or OAuth install flowshpua_xxx (auto via install)
Shopify App SecretWebflow App or wrangler secretshpss_xxx
Webflow CMS TokenWebflow App or wrangler secretxxx...
Webflow Collection IDWebflow Appxxx...
Resend API KeyWebflow App or wrangler secretre_xxx
Resend From EmailWebflow App or wrangler.tomlBrand <noreply@domain.com>
JWT Secretwrangler secret onlyopenssl rand -hex 32

Default CRM Tags

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

CategoryTags
statusActive, Inactive
tierVIP, Prospect
consentAccepts Marketing, Rejects Marketing, Accepts TOU, Accepts Privacy, Accepts Cookie
marketingEmail Subscribed, Email Unsubscribed, SMS Subscribed
segmentHigh Value, Returning, New Customer, At Risk
campaignCampaign

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