Author picture Phil Leggetter

Build Real-Time Order Notifications for Shopify with Webhooks and Ably WebSockets

Published


Real-time order notifications let storefront visitors see what products others are purchasing. This creates opportunities for product discovery and highlights trending items. In this tutorial, you'll build a notification system that displays live order updates on your Shopify storefront using webhooks and WebSockets.

This architecture provides production-grade reliability, observability, and developer experience through Hookdeck's event gateway. You'll use the event gateway to handle webhook ingestion from Shopify and queue events to Ably, a Remix application to transform data and remove Personally Identifiable Information (PII), and Ably WebSockets to deliver notifications to browsers in real-time.

Shopify Storefront Notifications

Architecture overview

This system uses the event gateway to ensure reliability and observability at every stage of the notification pipeline.

The complete flow:

Shopify Order (Webhook)

Hookdeck Event Gateway (Inbound Source)
    ↓ [Reliable Ingestion + Verification]
Remix Application (Local/Deployed)
    ↓ [Transformation: Remove PII]
Hookdeck Event Gateway (Outbound Queue)
    ↓ [Retry Logic + Observability]
Ably (WebSocket Connection)
    ↓ [Persistent Connection]
Browser Client (Theme Extension)
    ↓ [Display Notification Popup]

Services used:

  • Shopify: Emits orders/create webhooks when customers place orders
  • Hookdeck Event Gateway: Provides infrastructure and tooling to build and manage event-driven applications
  • Ably: Real-time messaging platform that maintains WebSocket connections and delivers events to browsers
  • Remix: Full-stack web framework (using the Shopify Remix app template) to handle webhook processing and data transformation

Why Hookdeck Event Gateway in both directions?

The key advantage is end-to-end observability in a single dashboard.

Inbound (Shopify → App):

  • Instant acknowledgment prevents webhook timeouts
  • Automatic Shopify signature verification
  • Retry logic if your app is temporarily down
  • Receive webhooks on localhost via the Hookdeck CLI

Outbound (App → Ably):

  • See every event published to Ably in the same dashboard
  • Track the complete journey: webhook received → transformed → published → delivered
  • Guaranteed delivery with exponential backoff on failures
  • Debug failures at any point in the pipeline

When debugging "why didn't a notification appear?", you have one place to check the entire flow. Did Shopify send it? ✓ Did your app receive it? ✓ Was PII removed? ✓ Did it publish to Ably? ✓ This unified visibility is invaluable for monitoring production systems.

Prerequisites

Before starting, you'll need:

Accounts:

Tools:

Knowledge:

  • Basic understanding of webhooks and Pub/Sub
  • Familiarity with React/Remix (or similar framework)

Estimated completion time: 30-45 minutes

Get the application running

Let's start by getting the application code and dependencies installed.

Clone and install dependencies

Clone the example repository and install dependencies:

git clone https://github.com/hookdeck/shopify-festive-notifications.git
cd shopify-festive-notifications

npm install

The project structure includes:

This is an extension-only app that provides real-time notifications through a theme extension.

Configure environment variables

Copy the example environment file:

cp .env.example .env

Update the .env file with your credentials:

# Hookdeck Configuration
HOOKDECK_API_KEY=your_hookdeck_api_key

# Ably Configuration
ABLY_API_KEY=your_ably_api_key

# Shopify Configuration (from your Shopify Partner Dashboard app)
SHOPIFY_API_KEY=your_shopify_api_key
SHOPIFY_API_SECRET=your_shopify_api_secret

Where to find each credential:

  • HOOKDECK_API_KEY: Get from Hookdeck Dashboard → Settings → Project → API Keys
  • ABLY_API_KEY: Get from Ably Dashboard → Your App → API Keys (format: appId.keyId:keySecret)
  • SHOPIFY_API_KEY and SHOPIFY_API_SECRET: Create a Shopify app in your Partner Dashboard → Apps → Create App

Set up Hookdeck connections

Run the automated setup script to create both inbound and outbound Hookdeck connections:

npm run setup

This script creates two connections:

1. Shopify Inbound Connection (Shopify → Hookdeck → App):

  • Source: shopify-webhooks with Shopify signature verification
  • Destination: CLI path /webhooks/orders/create
  • Retry rules: 5 retries with exponential backoff

2. Ably Outbound Connection (App → Hookdeck → Ably):

  • Source: shopify-notifications-publish (Publish API)
  • Destination: ably-rest-api at https://rest.ably.io/channels/shopify-notifications/messages
  • Authentication: Basic Auth with your Ably API key

The script automatically updates your shopify.app.toml with the generated Hookdeck source URL.

Verify the connections in the Hookdeck Dashboard.

Event Gateway Connections in the Hookdeck Dashboard

Start the development environment and install the app

You'll need two terminal windows running simultaneously.

Terminal 1: Start the Remix application

npm run dev

When you run this command for the first time, you'll be prompted to:

  1. Select or create a development store
  2. Install the app on that store

The command will then start the Remix app on localhost:3000. The orders/create webhook will be automatically registered to send events to your Hookdeck source URL.

Terminal 2: Start Hookdeck CLI

hookdeck listen 3000 shopify-webhooks

This creates a tunnel from Hookdeck sources to your local application. The CLI will show output like:

●── HOOKDECK CLI ──●

Listening on 1 source 1 connection [i] Collapse

shopify-webhooks
  Requests to https://hkdk.events/{id}
└─ Forwards to http://localhost:3000/webhooks/orders/create (shopify-orders-create)

💡 View dashboard to inspect, retry & bookmark events: https://dashboard.hookdeck.com/events/cli?{id}

Events [↑↓] Navigate ──────────────────────────────────────────────────────────────────────────
                                                                                                       
 Connected. Waiting for events...   

The Hookdeck source URL (https://hkdk.events/{id}) is already configured in your shopify.app.toml file by the setup script.

Enable the theme extension

After installation, enable the notification extension in your theme:

  1. Navigate to your development store's admin → Online Store → Themes
  2. Click "Customize" on your active theme
  3. In the theme editor, click the theme settings icon (☰)
  4. Go to "App embeds"
  5. Find "Live Notifications" and toggle it ON
  6. Click "Save"

The extension is now active and will display notifications when orders are created.

Understanding the code

Let's walk through the key components that make this notification system work.

Webhook handler (Inbound)

The webhook handler receives order creation events from Shopify via Hookdeck. Located in app/routes/webhooks.orders.create.tsx:

import type { ActionFunctionArgs } from "@remix-run/node";
import { authenticate } from "../shopify.server";
import { publishOrderNotification } from "../helpers/hookdeck-publisher";

export const action = async ({ request }: ActionFunctionArgs) => {
  // Shopify's authenticate.webhook verifies the signature and extracts webhook data
  const { topic, shop, admin, payload } = await authenticate.webhook(request);

  try {
    // Transform payload and publish to Hookdeck
    const result = await publishOrderNotification(payload, shop, admin);
    console.log("Order notification published successfully", result);

    return new Response();
  } catch (error) {
    console.error("Error publishing notification", error);

    // Re-throw to trigger Hookdeck retry
    throw error;
  }
};
  • authenticate.webhook(request): Shopify's built-in authentication verifies signatures and extracts the webhook topic, shop domain, GraphQL admin client, and payload
  • admin parameter: Used to fetch product images via GraphQL; may be undefined for test webhooks
  • Error handling: Re-throwing errors triggers Hookdeck's automatic retry logic for transient failures

Data transformation and PII removal

The transformation logic removes all personally identifiable information before publishing to the public real-time channel. Located in app/helpers/hookdeck-publisher.ts:

async function transformOrderToNotification(
  order: any,
  shopDomain?: string,
  admin?: any,
): Promise<OrderNotification> {
  // Extract line items without PII
  const lineItems: OrderNotificationLineItem[] = await Promise.all(
    (order.line_items || []).map(async (item: any) => {
      let image: string | undefined;

      // Fetch product image if admin client is available and product_id exists
      if (admin && item.product_id) {
        image = await fetchProductImage(admin, item.product_id);
      }

      return {
        name: item.name,
        title: item.title,
        quantity: item.quantity,
        price: item.price,
        sku: item.sku,
        product_id: item.product_id,
        variant_id: item.variant_id,
        vendor: item.vendor,
        image,
      };
    }),
  );

  // Calculate total item count
  const itemCount = lineItems.reduce((sum, item) => sum + item.quantity, 0);

  // Build notification object with all non-PII data
  const notification: OrderNotification = {
    created_at: order.created_at,
    currency: order.currency,
    total_price: order.total_price,
    subtotal_price: order.subtotal_price,
    total_tax: order.total_tax,
    total_discounts: order.total_discounts,
    item_count: itemCount,
    line_items: lineItems,
    shop: shopDomain || order.shop_domain || "",
    test: order.test || false,
  };

  return notification;
}

What's excluded:

  • Customer names, emails, phone numbers
  • Shipping/billing addresses
  • Order numbers (potential PII)
  • IP addresses
  • Payment details

What's included:

  • Order metadata (timestamps, currency, test flag)
  • Financial data (prices, tax, discounts)
  • Product information (names, quantities, SKUs, images)
  • Shop information

This ensures notifications can be safely displayed to all storefront visitors without exposing sensitive customer data.

Publishing to Hookdeck queue

After transformation, events are published to Hookdeck's Publish API, which queues them for delivery to Ably:

export async function publishToHookdeck(
  sourceName: string,
  data: any,
  headers?: Record<string, string>,
): Promise<HookdeckPublishResponse> {
  const url = "https://hkdk.events/v1/publish";

  const apiKey = process.env.HOOKDECK_API_KEY;
  if (!apiKey) {
    throw new Error("HOOKDECK_API_KEY environment variable is not set");
  }

  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`,
      "X-Hookdeck-Source-Name": sourceName,
      ...headers,
    },
    body: JSON.stringify({ data }),
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(
      `Hookdeck publish failed: ${response.status} ${response.statusText} - ${errorText}`,
    );
  }

  return await response.json();
}

export async function publishOrderNotification(
  order: any,
  shopDomain?: string,
  admin?: any,
): Promise<HookdeckPublishResponse> {
  // Transform order to remove PII and fetch product images
  const notification = await transformOrderToNotification(
    order,
    shopDomain,
    admin,
  );

  // Publish to Hookdeck
  const result = await publishToHookdeck(
    "shopify-notifications-publish",
    notification,
  );

  console.log("Order notification published successfully:", result.id);
  return result;
}

Why publish through Hookdeck instead of directly to Ably?

  • Retry logic: Automatic retries on failures with exponential backoff
  • Observability: Track both webhook ingestion AND outbound publishing in one dashboard
  • Debugging: See the complete event journey from Shopify → App → Ably
  • Rate limiting protection: Hookdeck queues events if Ably rate limits are hit

The X-Hookdeck-Source-Name header tells Hookdeck which source to publish to. The shopify-notifications-publish source is configured to forward events to the Ably REST API.

Browser client subscription

The browser-side code subscribes to the Ably channel and displays notifications when events arrive. Located in extensions/live-notifications/assets/notifications.js:

const createNotification = (orderData) => {
  console.log("Creating notification with data:", orderData);

  // Get the first line item for display
  const item = orderData.line_items && orderData.line_items[0];
  if (!item) {
    console.warn("No line items in order data");
    return;
  }

  // Create notification element
  const notification = document.createElement("div");
  notification.className = "order-notification";

  notification.innerHTML = `
    <button class="notification-close" onclick="this.parentElement.remove()">×</button>
    <div class="notification-header">🎉 New Order!</div>
    <div class="notification-content">
      ${item.image ? `<img src="${item.image}" alt="${item.name}" class="notification-image">` : ""}
      <div class="notification-details">
        <div class="notification-product">${item.name}</div>
        <div class="notification-price">
          ${orderData.currency} $${item.price} × ${item.quantity}
        </div>
        <div class="notification-shop">
          ${orderData.shop}
        </div>
      </div>
    </div>
  `;

  document.body.appendChild(notification);

  // Auto-remove after 10 seconds
  setTimeout(() => {
    notification.style.animation = "slideIn 0.3s ease-out reverse";
    setTimeout(() => notification.remove(), 300);
  }, 10000);
};

const connectAbly = async () => {
  const ably = new Ably.Realtime("YOUR_ABLY_API_KEY");
  
  ably.connection.once("connected", () => {
    console.log("Connected to Ably!");
  });

  const channel = ably.channels.get("shopify-notifications");
  await channel.subscribe((message) => {
    console.log("Message received:", message);

    const orderData = message.data;
    console.log("Order data:", orderData);

    if (orderData) {
      createNotification(orderData);
    }
  });
};

document.addEventListener("DOMContentLoaded", () => {
  connectAbly();
});
  • Ably connection lifecycle: Initialize once when DOM loads, clean up on page unload
  • Channel naming: Must match the channel configured in the Hookdeck outbound connection (shopify-notifications)
  • Order data: The order data is retrieved from the message.data property
  • Auto-dismiss: Notifications automatically remove themselves after 10 seconds
  • Security note: In production, use token authentication instead of embedding API keys in browser code

The notification styling uses CSS animations for the slide-in effect. See extensions/live-notifications/assets/notifications.css for the complete styling.

Testing the complete flow

Now that everything is set up, let's test the end-to-end notification system.

Create a test order

Create an order manually in your development store:

  1. Navigate to your store's online store
  2. Add a product to cart
  3. Complete checkout with test payment information

This ensures a real orders/create webhook is triggered with product data also available.

Verify in Hookdeck Dashboard

Open the Hookdeck Events dashboard to see the complete event flow.

What to look for:

  1. Inbound event from Shopify: Look for an event from the shopify-webhooks source

    • Click on the event to inspect the payload
    • Check the "Attempts" tab to see successful delivery to your app
    • Verify the status is "Delivered"
  2. Outbound event to Ably: Look for an event from the shopify-notifications-publish source

    • This is the event your app published after transformation
    • Check the payload to verify PII was removed
    • Verify it was successfully delivered to the ably-rest-api destination

This dual visibility is the key value of using Hookdeck in both directions. You can trace the complete journey of every order notification from Shopify → Your App → Ably → Browser.

Verify in browser

Open your storefront in a web browser. You should see:

  1. Browser console logs:

    Connected to Ably!
    Message received: {...}
    Order data: {...}
    Creating notification with data: {...}
    
  2. Notification popup: A notification should slide in from the side of the screen showing:

    • Product name
    • Product image (if available)
    • Price and quantity
    • Shop name
  3. Auto-dismiss: The notification should automatically disappear after 10 seconds

Deployment considerations

When you're ready to move from local development to production, you'll need to update your Hookdeck connections and deploy your application.

Deployment checklist

  • Deploy Remix application to hosting provider (Vercel, Fly.io, Railway)
  • Update Hookdeck connections for production (see below)
  • Set all environment variables in production environment
  • Configure Ably token authentication for browser clients (security best practice)
  • Test complete webhook flow in production
  • Verify notifications appear in storefront

Update Hookdeck connections for production

During development, you used CLI destinations. For production, you have two options:

Option 1: Update existing destination

You can update the existing shopify-orders-create-cli destinations to be of type HTTP and point to your production endpoint. This is quick but mixes dev and prod environments.

Option 2: Create new connections in a new Project (Recommended)

For better separation between development and production environments, create a new Hookdeck project for production. This gives you:

  • Isolated environments for dev and prod
  • Separate API keys and credentials
  • Independent monitoring and metrics
  • Clear separation of test vs. live data

To create new connections in production:

  1. Create a new Hookdeck project for production
  2. Update the HOOKDECK_API_KEY with the new project's API key
  3. Re-run the setup script in your production repository
  4. Configure the connections in the Hookdeck dashboard to use HTTP destinations for your deployed Remix app URL

The source URL will need to be updated in your Shopify webhook configuration when switching projects.

Production security notes

For browser-side Ably connection:

Don't expose your Ably API key in browser code. Instead, create a token authentication endpoint. See Ably's token authentication docs for detailed implementation.

Next steps

You've built a production-grade real-time notification system using Shopify webhooks, Hookdeck Event Gateway, and Ably WebSockets. The event gateway provides end-to-end observability in a single dashboard, making it easy to debug issues and monitor your notification pipeline.

Extend the system:

  • Add more webhook event types (inventory updates, product launches, etc.)
  • Implement notification personalization based on user behavior
  • Customize the notification UI to match your brand
  • Add analytics to track notification effectiveness

Related resources:


Author picture

Phil Leggetter

Head of Developer Relations

Phil is Head of Developer Relations at Hookdeck with expertise in real-time web technologies, co-author of 'Realtime Web Apps', and creator of the AAARRRP Developer Relations strategy framework.