| title | CRM Sync — PWA & Native App Commerce Setup Reference |
|---|---|
| description | Ship your CRM as an installable PWA and native app (iOS · Android · desktop) on Shopify + Webflow + Xano — with auth, real-time consent, GA4, and agent permissions. Everything you need to get it running, in order. |
| canonical | https://persephonepunch.github.io/crm-sync-setup/readme.html |
| category | Setup |
| date | 2026-07-06 |
| source | https://github.com/persephonepunch/crm-sync-setup/blob/master/README.md |
Ship your CRM as an installable PWA and native app (iOS · Android · desktop) on Shopify + Webflow + Xano — with auth, real-time consent, GA4, and agent permissions. Everything you need to get it 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.
CRM Sync PIM Sync Shopify Checklist [Strategy] (https://miro.com/app/board/uXjVHDMJNpA=/?share_link_id=653901982235) Accessibility & Machine-Index Spec Dawn → Horizon + GraphQL/GA4 Brief
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
- Cloudflare account (sign up free)
- Node.js 20 or newer (download)
- Wrangler CLI — Cloudflare's command-line tool
Install Wrangler:
npm i -g wranglerwrangler loginThis opens a browser window. Log in and authorize Wrangler.
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_STATEYou'll see output like:
{ binding = "CRM_STATE", id = "abc123..." }
Copy the id value — you'll need it in the next step.
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.
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)npm install
npx wrangler deploy --config wrangler.tomlYour worker will be live at: https://your-crm-worker.<your-account>.workers.dev
curl https://your-crm-worker.<your-account>.workers.dev/healthYou should see:
{"status":"ok","service":"crm-sync"}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.
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
- Xano account (sign up — free tier works)
- A workspace created in Xano
Create these 6 tables in Xano. The field names must match exactly.
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 |
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 |
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 |
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 |
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 |
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.
- 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) |
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].
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.
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.
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).
- 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
In your shopify.app.crm-sync.toml:
[access_scopes]
scopes = "read_customers,write_customers,customer_read_customers,customer_write_customers"Add your worker's callback URL:
[auth]
redirect_urls = [
"https://your-worker.workers.dev/auth/callback"
]Deploy the app config:
npx shopify app deploy- 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 required since April 1, 2026 for all public Shopify apps. The worker automatically refreshes the access token before it expires — no manual rotation needed.
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_TOKENThe app secret is used for OAuth code exchange. The
shpua_access token is obtained automatically via the install flow.
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.
- 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
- 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
On the same Customer Account API page, add your Worker's callback URL:
https://your-crm-worker.<account>.workers.dev/auth/shopify/callback
| 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-07"
[[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
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)
Setup Steps: Google OAuth ("Sign in with Google")
- 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")
Under Authorized redirect URIs, add:
https://your-crm-worker.<account>.workers.dev/auth/google/callback
Under Authorized JavaScript origins, add your Webflow site:
https://your-site.webflow.io
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_SECRETIn 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.
- Go to Google Analytics
- Navigate to Admin > Data Streams > Web
- Copy the Measurement ID (looks like
G-XXXXXXXXXX)
- In the same Data Stream, scroll to Measurement Protocol API secrets
- Click Create
- Name it (e.g., "CRM Sync Server")
- Copy the secret value
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_SECRETAnd add to wrangler.toml:
GA4_MEASUREMENT_ID = "G-XXXXXXXXXX"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 |
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"
- VIP Customers:
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.
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.
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>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.
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.
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.
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.
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
| 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.
In GTM, create one tag for all form types:
| 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 |
| 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 |
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 |
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.
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" |
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'));- 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
- Console:
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.
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
Setup Steps
- Go to Webflow Site Settings > Integrations > API Access
- Click Generate API Token
- Required scopes: CMS read/write
- Copy the token
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.
curl -X POST https://your-worker.workers.dev/admin/init-tag-system?step=webflowThis creates the CRM Tags collection and populates it with the 17 default tags.
Set in the Webflow App > Config tab:
- CMS API Token: the token from Step 1
- Customers Collection ID: from Step 2
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.
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.
Setup Steps
- Go to resend.com and sign up
- Verify your sending domain
You must verify the domain in Resend before you can send from it.
- Go to resend.com/api-keys
- Create a new API key
- Copy it — it starts with
re_
Set the API key:
wrangler secret put RESEND_API_KEYSet 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>
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
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 |
A2A (Agent-to-Agent) is Google's open protocol for AI agents to discover and communicate with each other. CRM Sync implements A2A so that external AI agents — shopping assistants, support bots, recommendation engines — can programmatically discover your store's capabilities, read customer profiles, browse products, and check order history.
Two discovery endpoints:
-
UCP Discovery Manifest (
GET /.well-known/ucp) — Declares all capabilities the worker supports (identity linking, consent management, product catalog, checkout, orders, analytics, etc.). Capabilities are generated dynamically based on which services are configured for the tenant. -
A2A Agent Card (
GET /.well-known/agent-card.json) — A standard agent card that AI platforms use to discover CRM Sync as a commerce service agent. Declares 5 skills: Identity Linking, Consent Management, Customer Tag Sync, Product Catalog, and Checkout Session.
Commerce API endpoints exposed via A2A:
| Endpoint | Method | Auth | What it does |
|---|---|---|---|
/commerce/identity |
GET | JWT | Unified customer profile with linked providers (email, Google, Shopify), consent status, tags, segment |
/commerce/products |
GET | Public | Search product catalog with pagination and collection filtering |
/commerce/orders |
GET | JWT | List customer's order history (cached 5 min) |
/commerce/orders/:id |
GET | JWT | Single order detail with line items and fulfillment status |
/ucp/consent-history |
GET | JWT | Paginated consent audit trail |
/ucp/tags |
POST | JWT | Add/remove customer tags (flows to Shopify, Webflow CMS, GA4) |
Customer consent required: A2A access is gated behind the consent_a2a flag. Customers must explicitly enable "A2A Agent Access" in their account preferences before any AI agent can read their profile or commerce data. The consent toggle appears in the Account page and Dashboard embeds.
When A2A consent is enabled, the UCP manifest dynamically adds a2a_agent_access to its capabilities list.
Setup Steps
Once your worker is deployed with the services configured (Tabs 1–7), the discovery endpoints are automatically available.
# UCP Discovery Manifest
curl https://your-worker.workers.dev/.well-known/ucp
# A2A Agent Card
curl https://your-worker.workers.dev/.well-known/agent-card.jsonThe UCP manifest lists all capabilities based on your configured services. The Agent Card describes the 5 skills an AI agent can use.
Customers enable A2A access from their account page or dashboard. The toggle is labeled "A2A Agent Access" with the description: "Allow AI agents to read your profile and consent via A2A protocol."
You can also set the default consent state for new customers from the Webflow extension's Auth tab under "Default Consent Options."
# Get a JWT token first (via login)
TOKEN=$(curl -s -X POST https://your-worker.workers.dev/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"testpass123"}' | jq -r '.token')
# Query the commerce identity endpoint
curl -H "Authorization: Bearer $TOKEN" \
https://your-worker.workers.dev/commerce/identityResponse includes linked providers, consent status (including a2a and ap2 flags), tags, segment, and language preference.
# Public endpoint — no auth required
curl "https://your-worker.workers.dev/commerce/products?shop=your-store.myshopify.com&query=shirt&first=10"
# Filter by collection
curl "https://your-worker.workers.dev/commerce/products?shop=your-store.myshopify.com&collection=summer-2026"curl -H "Authorization: Bearer $TOKEN" \
https://your-worker.workers.dev/commerce/ordersThe Agent Card at /.well-known/agent-card.json follows the A2A spec:
{
"name": "CRM Sync Commerce Agent",
"description": "Multi-tenant customer identity, consent, and commerce orchestration across 7 services",
"version": "1.0",
"protocol_version": "0.1",
"capabilities": {
"streaming": false,
"pushNotifications": false,
"stateTransitionHistory": true
},
"authentication": {
"schemes": ["bearer"],
"credentials": {
"bearer": {
"description": "JWT token from /auth/login, /auth/google/callback, or /auth/shopify/callback, or admin key"
}
}
},
"skills": [
{ "id": "identity-linking", "name": "Identity Linking", ... },
{ "id": "consent-management", "name": "Consent Management", ... },
{ "id": "tag-sync", "name": "Customer Tag Sync", ... },
{ "id": "product-catalog", "name": "Product Catalog", ... },
{ "id": "checkout-session", "name": "Checkout Session", ... }
]
}AP2 (Agent Payments Protocol) ensures that AI agents cannot make purchases without explicit human authorization. It extends the A2A protocol with cryptographically signed payment mandates — a customer must create a mandate (spending limit + scope + expiry) before any agent can initiate a checkout on their behalf.
How it works:
Customer enables AP2 consent in account preferences
→ Customer creates a mandate: "Up to $100 for any product, expires in 24h"
→ Worker generates HMAC-SHA256 signed mandate, stores in KV
→ AI agent calls POST /commerce/checkout with mandate_id
→ Worker validates: mandate is active, not expired, covers the purchase amount/scope
→ If valid: Shopify checkout created via Storefront Cart API
→ If invalid: 403 with reason (expired, exceeded, wrong scope)
→ Mandate usage logged to consent audit trail + GA4
AP2 consent gate: If consent_ap2 is not granted, POST /commerce/mandates returns 403 "AP2 agent payments consent required". The customer must enable it first.
Endpoints:
| Endpoint | Method | Auth | What it does |
|---|---|---|---|
/commerce/mandates |
POST | JWT | Create a new payment mandate |
/commerce/mandates/:id |
GET | JWT or Admin | Retrieve mandate status and details |
/commerce/checkout |
POST | JWT | Create checkout session (optionally with mandate_id) |
/commerce/checkout/:id |
GET | JWT or Admin | Get checkout session status |
Mandate schema:
{
"mandate_id": "uuid",
"customer_id": 123,
"type": "purchase",
"max_amount": "100.00",
"currency": "USD",
"scope": "all | collection:summer-2026 | product:gid://shopify/Product/123",
"status": "active | used | expired | revoked",
"created_at": "2026-05-24T10:00:00Z",
"expires_at": "2026-05-25T10:00:00Z",
"signature": "base64-hmac-sha256"
}GA4 events fired:
ucp_mandate_created— when a mandate is created (includesmandate_id,max_amount,currency,scope)ucp_mandate_used— when a mandate is used for checkoutucp_checkout_created— when a checkout session is createducp_checkout_completed— when a checkout session completes
Setup Steps
Mandates use the tenant's JWT secret for HMAC-SHA256 signing. Checkout uses the Storefront Cart API (with Draft Order fallback).
Customers enable AP2 from their account page. The toggle is labeled "AP2 Agent Payments" with the description: "Allow AI agents to create checkout sessions using signed mandates."
You can set the default for new customers in the Webflow extension's Auth tab.
TOKEN="<customer-jwt>"
curl -X POST https://your-worker.workers.dev/commerce/mandates \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"max_amount": "100.00",
"currency": "USD",
"scope": "all",
"expires_in_hours": 24
}'Response (201):
{
"mandate_id": "a1b2c3d4-...",
"customer_id": 123,
"type": "purchase",
"max_amount": "100.00",
"currency": "USD",
"scope": "all",
"status": "active",
"created_at": "2026-05-24T10:00:00Z",
"expires_at": "2026-05-25T10:00:00Z",
"signature": "base64..."
}curl -X POST https://your-worker.workers.dev/commerce/checkout \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"line_items": [
{ "variant_id": "gid://shopify/ProductVariant/123", "quantity": 1 }
],
"mandate_id": "a1b2c3d4-..."
}'The worker validates the mandate covers the purchase before creating the Shopify checkout.
curl -H "Authorization: Bearer $TOKEN" \
https://your-worker.workers.dev/commerce/mandates/a1b2c3d4-...After creating a mandate and checkout, the customer's UCP Dashboard shows:
- Consent Status: "AP2 Agent Payments: Granted"
- Consent History: Timestamped entries for
mandate_createdandmandate_used - Protocol Status: AP2 capability listed as active
| Control | Implementation |
|---|---|
| Consent gating | consent_ap2 must be true before any mandate creation |
| Cryptographic signing | HMAC-SHA256 with tenant JWT secret; signature covers mandate_id:customer_id:max_amount:expires_at |
| Expiry enforcement | Mandates stored in KV with TTL matching expires_in_hours (default 24h); auto-deleted on expiry |
| Ownership verification | Customers can only view their own mandates; admin key required for cross-customer access |
| Amount validation | Checkout with mandate validates purchase total against max_amount |
| Scope restriction | Mandates can be scoped to all, a specific collection, or a specific product |
| Audit trail | All mandate events logged to consent_records and pushed to GA4 |
| Tenant isolation | Mandates stored at tenant:{shop}:mandate:{id} — no cross-tenant access |
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 |
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.