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:
- 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.
<details> <summary><strong>Setup Steps</strong></summary>
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"}
</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:
- 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
<details> <summary><strong>Setup Steps</strong></summary>
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 |
| 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_tagsanduser_tag_mapmanually, you can use the admin endpoint after deploying the worker:curl -X POST https://your-worker.workers.dev/admin/init-tag-system?step=xanoThis auto-creates both tables and seeds the 17 default tags.
Step 2 — Get Your API Credentials
- Go to your Xano workspace
- Navigate to Settings > API Keys
- Click Create a Meta API key with full access
- 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.
</details>
Tab 3: Shopify (Store Integration)
What is this?
The Shopify integration does four things:
- 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.
- 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.
- Customer Account OAuth — Lets users sign in with their Shopify account using PKCE (no client secret needed).
- 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
- Go to Shopify Dev Dashboard (partners.shopify.com > Apps)
- Click Create App
- Name it (e.g., "CRM Sync")
- 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_productsis required for the product catalog (/commerce/products) and Shop page embed.read_ordersis 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)
- Visit
https://your-worker.workers.dev/auth/install?shop=your-store.myshopify.com - Approve the OAuth prompt in Shopify
- 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 - The worker automatically registers
CUSTOMERS_CREATEandCUSTOMERS_UPDATEwebhooks 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 viaPOST /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".
</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
- In Shopify Admin, go to Sales channels (left sidebar)
- Click Headless (if not installed, add it from the sales channels list)
- Click Create storefront or select your existing headless storefront
Step 2 — Get Client ID and Shop ID
- On the Headless channel page, under Manage API access, click Manage next to Customer Account API
- Copy the Client ID — a UUID like
890afa5e-c87b-496e-... - 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.
</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:
POST /gdpr/customer-redact— deletes/anonymizes user dataPOST /gdpr/data-request— compiles all stored data for a userPOST /gdpr/shop-redact— acknowledges shop-level data removal
</details>
Tab 4: Google (Analytics, OAuth & GA4 Segments)
What is this?
The Google integration has three parts:
- Google OAuth — "Sign in with Google" button via OpenID Connect.
- 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.
- 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 (includestags_added,tags_removed,campaign_tagsparams)crm_sync— fired during the 15-minute cron sync from Shopify (includessource: shopify_cron)
<details> <summary><strong>Setup Steps: Google OAuth ("Sign in with Google")</strong></summary>
Step 1 — Create OAuth Credentials
- Go to Google Cloud Console
- Create a project (or select an existing one)
- Navigate to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Application type: Web application
- 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.
</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
- Go to Google Analytics
- Navigate to Admin > Data Streams > Web
- Copy the Measurement ID (looks like
G-XXXXXXXXXX)
Step 2 — Create an API Secret
- In the same Data Stream, scroll to Measurement Protocol API secrets
- Click Create
- Name it (e.g., "CRM Sync Server")
- 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:
- Go to GA4 Admin > Custom definitions > Create custom dimension
- 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:
- Go to GA4 Admin > Audiences > New Audience
- 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.
</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
gtagis 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 |
</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 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
- Open your Webflow site with a
data-crm-formform - Open Chrome DevTools > Network tab
- Submit the form with a test email
- Check:
- Console:
dataLayer.filter(e => e.event === 'crm_form_submit')— shows form_type, email, session_id - Network:
POST /ucp/tagswith{form_type}_subscribedtag - Network:
POST /auth/consent-syncwithmethod: {form_type}_form - GTM Preview:
crm_form_submittrigger fires - GA4 DebugView:
crm_form_submitevent 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.
</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:
- Customers — synced from Xano on every cron run (name, email, provider, consent status, order data, tags)
- 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
- Go to Webflow Site Settings > Integrations > API Access
- Click Generate API Token
- Required scopes: CMS read/write
- 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 | ||
| 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
</details>
Campaign Tags from Dashboard
When a user adds a tag like "new campaign" from the UCP dashboard, it immediately:
- Creates the tag in Xano (
crm_tagstable, category:campaign) - Assigns it to the user in the join table (
user_tag_map) - Pushes it to Shopify as a customer tag + metafield
- Creates it in the Webflow CRM Tags collection
- Pushes it to GA4 as a
crm_campaignuser property
No cron wait — the full channel flow happens in one request.
Tab 6: Email (Resend)
What is this?
Resend handles transactional emails:
- Password reset — When a user clicks "Forgot password?", the worker sends a reset link (1-hour expiry).
- 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
- Go to resend.com and sign up
- Verify your sending domain
You must verify the domain in Resend before you can send from it.
Step 2 — Get Your API Key
- Go to resend.com/api-keys
- Create a new API key
- 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:
| 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 |
Tab 8: Agentic Commerce — Entitlements, Consent & Enterprise Add-ons
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
- Scoped read key — mint an
entitlement:readkey in Xano and provide it asXANO_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. - 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.
- 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.
Entitlements & consent
- An entitlement is one record per subject (
tenant/user/agent) withplan_tier,features[],caps(a2a / ap2 / mandate_max_amount / channels), and an explicitconsentobject. It carries a monotonicstate_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.
Header / Footer embed codes
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
/settingsenv 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-onlyon Stage/Prod to avoid mutations);/setupshows 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 onconsent.clean_roomat 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:
- 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 viawrangler secret putat 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_KEYalways 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.
| 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 |
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.