Shopify end-to-end setup
Wire up Buddy Assist on a live Shopify storefront — widget_config, backend tunnel, bundle hosting, theme snippet, and verification.
This guide walks through every piece required to run Buddy Assist on a Shopify storefront end-to-end:
- Create the
widget_configrow (mints thepublic_widget_key) - Expose Clairvoyance via a Cloudflare tunnel
- Host the widget bundle via a second tunnel
- Embed the snippet in
theme.liquid - Verify in browser
Step 1 — create the widget_config row
Each storefront needs exactly one widget_config. It mints the public_widget_key (the tenant attribute on the custom element) and lists the allowed_origins that may create sessions.
Save the script below and run it once. It is idempotent — if a row already exists for that reseller_id + merchant_id pair it prints the existing key and exits.
# /tmp/widget_config_create.py
import asyncio, secrets, asyncpg
async def main():
conn = await asyncpg.connect(
host="localhost", port=5432,
user="YOUR_DB_USER", password="YOUR_DB_PASSWORD",
database="zephyr"
)
try:
existing = await conn.fetchrow(
"SELECT id, public_widget_key FROM widget_config "
"WHERE reseller_id=$1 AND merchant_id=$2",
"BB_SHOPIFY", "your-store.myshopify.com"
)
if existing:
print(f"already exists: id={existing['id']} key={existing['public_widget_key']}")
return
# 40-byte URL-safe token (per migration 030)
key = secrets.token_urlsafe(30)
new_id = await conn.fetchval(
"""INSERT INTO widget_config(
reseller_id, merchant_id, public_widget_key,
template_id, allowed_origins, active)
VALUES ($1,$2,$3,$4::uuid,$5,true)
RETURNING id""",
"BB_SHOPIFY",
"your-store.myshopify.com",
key,
"YOUR_TEMPLATE_UUID",
[
"https://your-custom-domain.com",
"https://www.your-custom-domain.com",
"https://your-store.myshopify.com",
"http://localhost:5180" # remove before production
]
)
print(f"created id={new_id}\npublic_widget_key={key}")
finally:
await conn.close()
asyncio.run(main()) uv run python /tmp/widget_config_create.py Copy the printed public_widget_key — you’ll paste it into the theme snippet in step 4.
Step 2 — expose Clairvoyance via Cloudflare Tunnel
Run the backend, then open a tunnel so the storefront (a remote browser) can reach it.
# Terminal A — start Clairvoyance
cd /path/to/clairvoyance
uv run python run.py
# binds 0.0.0.0:8000
# Install cloudflared once if needed
brew install cloudflare/cloudflare/cloudflared
# Terminal B — tunnel the backend
cloudflared tunnel --url http://localhost:8000
# prints: https://random-words-1234.trycloudflare.com Note the printed URL — this is your api-base.
Next, add the storefront origins to Clairvoyance’s CORS allow-list so browsers don’t get blocked:
# clairvoyance/.env
CORS_ALLOWED_ORIGINS=https://buddy.breezelabs.app,...,https://your-custom-domain.com,https://www.your-custom-domain.com,https://your-store.myshopify.com,http://localhost:5180 Restart uv run python run.py — static.py reads CORS_ALLOWED_ORIGINS once at boot.
Step 3 — host the widget bundle
The storefront HTML needs to load assist.js. Build the widget and serve it behind a second tunnel.
# Build the widget bundle (from the loom monorepo root)
pnpm --filter @juspay/breeze-buddy-assist build
# produces packages/breeze-buddy-assist-widget/dist/assist.js
# Serve dist/ on a static port
cd packages/breeze-buddy-assist-widget/dist
python3 -m http.server 5180 &
# Terminal C — tunnel the bundle server
cloudflared tunnel --url http://localhost:5180
# prints: https://other-words-5678.trycloudflare.com Note this second URL — the theme snippet will load assist.js from it.
Step 4 — embed the snippet in theme.liquid
In Shopify Admin → Online Store → Themes → Actions → Edit code → layout/theme.liquid.
Add the following block just before </body> (not inside <head>) — async scripts + custom-element upgrades work best after the HTML is parsed:
<!-- Breeze Buddy Assist widget -->
<script src="https://other-words-5678.trycloudflare.com/assist.js" async></script>
<breeze-buddy-assist
tenant="PASTE_PUBLIC_WIDGET_KEY_HERE"
shop="{{ shop.permanent_domain }}"
api-base="https://random-words-1234.trycloudflare.com"
modes="text"
default-mode="text"
theme="light"
position="bottom-right"
launcher-style="solid"
></breeze-buddy-assist> Replace:
| Placeholder | Value |
|---|---|
https://other-words-5678.trycloudflare.com/assist.js | Your bundle tunnel URL + /assist.js |
PASTE_PUBLIC_WIDGET_KEY_HERE | Key printed in step 1 |
https://random-words-1234.trycloudflare.com | Your backend tunnel URL |
{{ shop.permanent_domain }} — it resolves to the correct value automatically.Save the theme. Changes to theme.liquid go live for the assigned domain immediately.
Step 5 — verify in browser
Open your storefront with DevTools (F12) open.
Network tab
assist.jsshould load with HTTP 200.- A
POST .../widget/sessionfires when you click the launcher — response is201with awidget_token.
Functional test
- Open the widget → ask “what shirts do you have” → a tile carousel should render.
- Add a product to cart — after the cart UI renders:
- Application → Cookies → your-domain.com — a cart cookie should appear with the Shopify cart token.
- In the console:
await fetch('/cart.js').then(r=>r.json())— the added item should appear. - Navigate to
/cart— the storefront cart page should reflect the item.
Common gotchas
Cookie domain mismatch
The widget sets document.cookie = 'cart=<token>; path=/' on the page origin. If your storefront has both domain.com and www.domain.com, the cookie only covers the origin it was set on. To share it, the template’s tool_ui_instructions for create_cart/update_cart/get_cart would need a domain=.domain.com prop on the SideEffect — contact the team if you hit this.
Safari ITP
First-party cookies written via document.cookie from the widget (which runs on the storefront origin) are not subject to Safari ITP. The api-base Cloudflare tunnel is a different domain, but it only carries CORS requests — it never sets cross-site cookies. So ITP does not apply here.
Mixed content
Your storefront is https and both tunnel URLs are https. Never point api-base or the <script src> at a bare http://localhost URL — browsers block mixed content on live https pages.
Shopify password page
Confirm the store has no password-page protection before testing. The widget can’t initiate a session from behind a Shopify password page.