Tutorial

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:

  1. Create the widget_config row (mints the public_widget_key)
  2. Expose Clairvoyance via a Cloudflare tunnel
  3. Host the widget bundle via a second tunnel
  4. Embed the snippet in theme.liquid
  5. 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.

`allowed_origins` must include both the canonical domain and the `.myshopify.com` origin so the Shopify theme-editor preview works. Strip `localhost` entries before shipping to production.

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.pystatic.py reads CORS_ALLOWED_ORIGINS once at boot.

The free `trycloudflare.com` tunnel URL rotates every time you restart `cloudflared`. If you tear down terminals, update the theme snippet's `api-base` and the CORS list. For a stable hostname, use a **named tunnel**: `cloudflared tunnel --name my-backend --url http://localhost:8000`.

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.

Two tunnels is the simplest setup. If you prefer a single tunnel, run a local reverse proxy that mounts `/assist.js` from port 5180 and `/api/*` from port 8000, then create one tunnel pointing at the proxy.

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:

PlaceholderValue
https://other-words-5678.trycloudflare.com/assist.jsYour bundle tunnel URL + /assist.js
PASTE_PUBLIC_WIDGET_KEY_HEREKey printed in step 1
https://random-words-1234.trycloudflare.comYour backend tunnel URL
The `shop` attribute must be the `.myshopify.com` permanent domain, **not** the custom domain. Shopify's `/api/ucp/mcp` endpoint only lives under that origin. Use the Liquid variable {{ shop.permanent_domain }} — it resolves to the correct value automatically.

Save the theme. Changes to theme.liquid go live for the assigned domain immediately.

To test without affecting live traffic, **duplicate the theme first** and embed the snippet in the copy. Use the "Preview" button in Shopify Admin — the `.myshopify.com` origin is covered by your `allowed_origins`, so the preview will work.

Step 5 — verify in browser

Open your storefront with DevTools (F12) open.

Network tab

  • assist.js should load with HTTP 200.
  • A POST .../widget/session fires when you click the launcher — response is 201 with a widget_token.

Functional test

  1. Open the widget → ask “what shirts do you have” → a tile carousel should render.
  2. 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

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.

Was this helpful?