Pizza ordering app with Azure Functions

The Ably Realtime service provides a “pub/sub” (publish/subscribe) messaging system with which you can build sophisticated workflows and dataflow pipelines to meet the needs of your application. This can be as simple as publishing messages to named channels of your choice, and having users and server processes subscribe to those channels and respond with further messages as needed. But what if you don’t have a server to coordinate all of this, or simply don’t want to manage that side of things?

Even though Ably provides a full suite of authentication and authorization features to make sure users can only do what they’re permitted to do, we still need a way to connect your messaging workflow to your business logic – something that would normally happen on your application’s server. Doing so from a client-side browser application is risky, as it means exposing your business logic and other potentially sensitive data and services to the user.

With the rise of serverless solutions such as Microsoft Azure Functions, the problem goes away. Create server functions that contain your business logic, credentials and other sensitive data, hook them up to your application, and all of the sensitive work is hidden away from the user, not to mention being managed by world-class infrastructure providers such as Amazon, Google or Microsoft.

Ably’s Webhooks feature provides a way to define custom criteria that will automatically trigger different kinds of actions in response to channel messages, channel lifecycle events (such as a channel being created) and presence events (such as users entering or leaving a channel), and is how we’ll integrate with your server functions without exposing them to your users.

What we’ll be building

For this tutorial, we’ll be using Webhooks events in response to incoming messages in order to automatically invoke your own Azure functions. We’re going to build a simple pizza ordering system with a virtual pizza assistant chat bot. When a new customer hits your website, they’ll be assigned a random ID, and their messages will be published to a private channel created just for that customer. A webhook triggered by those messages will call your Azure functions to invoke the pizza chat bot to take their order. Using the Ably REST API, it will post its responses back on another channel that the web application subscribes to.

To get you up and running quickly, we’ve implemented a simple JavaScript-based pizza chat bot and made it available via this tutorial’s repository. The chat bot helps each customer design their own pizza, keep track of the order price, and then finalize the pizza order when they’re finished. The nlp compromise npm package is used for basic language parsing, and fuzzyset.js gives the bot the ability to cope with spelling mistakes, typographical errors and other textual variations it might encounter.

The details of the chat bot’s logic are beyond the scope of this tutorial — you’ll simply be referencing it from your Azure function. You are welcome to explore the chat bot’s source code (see pizza.js in the tutorial repository) and modify it as you see fit. Note also that while the script implies that a completed order has been placed, it is left as an optional exercise to actually dispatch the order to an endpoint, such as an Ably Queue that you might have configured for processing orders. For more sophisticated natural language capabilities, we’d recommend checking out Microsoft’s Azure Bot Service.


Diagram of the tutorial project's architecture


Animated image of the browser app in action

Step 1: Account setup

(a) Sign up with Microsoft Azure

The first thing you’ll need to do is make sure you have an Azure account with which you can create your server function.

If you don’t have an Azure account, create a free one here.

(b) Create an Ably API key

We’re going to need an API key for use by your browser application, and for the Azure function to allow it to post a response back to Ably. For our purposes, only one API key is required. If you extend the tutorial with your own functionality, you may wish to create a separate API key for the Azure function if you need it to have a different set of permissions than those assigned for use by the browser app.

First, log into your Ably dashboard, find the app you’re going to be using for this tutorial (the “Sandbox” app is probably fine), then click “Manage App”.


Manage your Ably app

Select the “API Keys” tab, and click “Create a new API key”:


Create a new API key

  1. Enter a name for your API key
  2. Select only the “publish” and “subscribe” options
  3. Select “Selected channels and queues”, and enter the channel prefix pizza:* to ensure that the API key can only access channels that start with pizza:.
  4. Click the “Create key” button


Fill out the API key form

Scroll down if necessary, and copy and paste your new API key to a text file for later use in your application.


Copy the API key

Step 2: Azure function setup

Log into your Azure account.

  1. Click the “New” menu item
  2. Click the “Compute” category
  3. Click the “Function App” option


Create a new function app

Azure requires that you create a “function app” before you can create a function. The app doesn’t consume any resources (except when your function is called) but it allows you to group multiple functions together as part of a single project, and provides a common HTTPS endpoint and other settings that are shared amongst the functions within that app.

Note that this tutorial was built with Windows. If you’re deploying on Linux (or otherwise), some configuration options may differ slightly from what is described below. Use your best judgement in each case.

  1. Enter a name for your function app. You’ll need to choose a name that is unique to you, and distinct from the name displayed in the screenshot below.
  2. Create a new resource group of the same name.
  3. Configure any other options you like, then click the “Create” button.
  4. Azure will create your app. Once it has done so, expand the app using the arrow button beside the app name in the left menu.
  5. From the “Functions” menu, click the large plus button to create your function.


Define function app details

Azure requires a few details before it can create your function.

  1. Select the “HTTP Trigger” template. A form will then be displayed to the right.
  2. Select “JavaScript” from the language dropdown.
  3. Give your function a name, such as “InvokePizzaAssistant”.
  4. Choose “Function” from the final dropdown.
  5. Click the “Create” button.


Create a function

In the interface displayed, you’ll see a space to edit your function, and a tab to the right that you can click to view and upload additional JavaScript files. By default, your function code is stored in the index.js file.

In the code editor, update the code editor with the following script, making sure to update the line containing “YOUR_API_KEY_HERE” with your actual API key:

const Ably = require('ably');
const processMessage = require('./pizza');

module.exports = function (context, req) {
  if (req.body && req.body.customerId) {
    const message = req.body.message;
    const order = req.body.context;
    const customerId = req.body.customerId;
    const response = processMessage(req.body.message, order);

    // Note that we use uses Ably.Rest here, not Realtime, because we don't want
    // to start a websocket connection to Ably just to publish a single response;
    // doing so would be inefficient.
    const ably = new Ably.Rest('YOUR_API_KEY_HERE');

    // Get the pizza bot's response channel
    const channel = ably.channels.get('pizza:bot:' + customerId);

    // Publish the response, and handle any unexpected errors
    channel.publish('bot', response, err => {
      if (err) {
        context.log(err); // Use the Azure logging console to see this output
        context.res = {
          status: 500,
          body: 'Error publishing response back to Ably'
        };
      }
      else {
        context.res = { status: 200, body: 'OK' };
      }
      context.done();
    });
  }
  else {
    context.res = {
      status: 400,
      body: "Invalid request format"
    };
    context.done();
  }
};

Create a folder on your local machine, then navigate to that folder from your console or terminal. Create a new package.json file using npm init. Next, install the project dependencies using npm install ably compromise fuzzyset. Once you have done this, your package.json file will look something like this:

{
  "name": "ably-pizza-tutorial",
  "version": "1.0.0",
  "description": "Ably Integrations Function Tutorial",
  "dependencies": {
    "ably": ">=1.2",
    "compromise": "^11.2.1",
    "fuzzyset": "0.0.4"
  }
}

Note that while the above package.json file can be found in the tutorial repository, the package versions it references may become outdated if we haven’t updated the tutorial repository in a while (particularly the ably package, which is updated more frequently than the tutorial repository). We recommend performing the install step above, which will ensure that you are referencing up-to-date package versions.

From the tutorial repository, download the pizza.js file.

Follow the instructions on the Node version and package management Azure documentation page to upload the package.json file and install the prerequisite npm packages required by the chat bot script.

Next, click the “View files” tab to the far right of the Azure function code editor, then the “Upload” button. Select and upload the pizza.js script referenced above, so that it sits alongside the index.js file.

When the script files are uploaded, click the “Test” option right above the file list. In the “Request body” editor, enter the following JSON code:

{
  "customerId": "abc123",
  "message": "I'd like a pepperoni pizza, please"
}

In the lower right, you can then click the “Run” button. If everything described above has been configured correctly, you should see a JSON response message in the “Output” box. It’ll include a message from the chat bot, and some contextual metadata required to maintain the customer’s order state as the conversation progresses.

Finally, Ably requires anonymous access to your function in order to call it. In the left menu under your function’s name, select the “Integrations” option, then change “Authorization level” dropdown box to “Anonymous”.


Authorization level should be 'Anonymous'

Step 3: Configure a webhook

The next thing is to set up a webhooks, which we need in order to relay channel messages from the customer to our Azure function for processing. We’ll actually be using two channels – one outbound, and one inbound. We’ll do this to ensure that only the customer’s communications are sent to Azure, and so that the browser app need only process messages from the chat bot. Channels only consume resources while in use, so using two channels in this way is not a problem.

Just as in step 1-b of this tutorial, go to the dashboard for the Ably app you’re using. From there, click the “Integrations” tab, followed by “New Integration Rule”:


Create a new Integration rule

Choose the “Webhook” option:


Select the Webhook option

Choose the “Azure” option:


Select the Azure option

Fill out the form to provide Ably with the details it needs to invoke your Azure function.

  1. Enter the name of the Azure Function app you created in step 2. In the screenshot it was “ably-pizza-tutorial”, but you will have entered something of your own.
  2. Enter InvokePizzaAssistant – the name of your Azure function.
  3. Make sure “Message” is set as the source.
  4. Enter a regular expression that the webhooks will use to whitelist the Ably channels for which the event should be triggered. ^pizza:customer: means that the channel name must begin with the string pizza:customer:, with no restrictions on the characters that follow.
  5. Ensure that the “Enveloped” checkbox is unchecked, so that only the raw JSON from the app gets sent to Azure.
  6. Click the blue “Create” button.


Fill out the form

Your webhook will now be created, and you’ll be able to continue developing your app.

Step 4: Developing the browser app

The browser app will be a simple text-based conversation with the Azure-hosted chat bot. We’ll have a list of messages published by the customer and the chat bot, followed by a text box in which the user can type their responses to the chat bot’s messages. We’ll also include an initial message that serves as the default greeting that the pizza store will give to each new customer.

In the repository for this tutorial, in addition to the completed code further below, you’ll find the CSS to go with the code below, and a subtle background image to complement the app and give it some visual polish.

Note that all of the code below can be found in this tutorial’s repository, if you just want to skip to the end and see the application running.

(a) Getting the application skeleton in place

  1. Create a new project directory on your hard drive
  2. Create a blank file named app.js in your project directory. We’ll edit it later.
  3. Either create a blank file named app.css in your project directory, or copy the pre-made file of the same name from the repository.
  4. Create an HTML file named index.html for the user interface. Start with the code below:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Ably's Pizza</title>

  <script src="https://cdn.ably.com/lib/ably.min-1.js"></script>

  <link rel="stylesheet" href="app.css">
</head>
<body>
  <div id="chat-interface">
    <h1>Ably's Realtime Pizza</h1>

    <div id="chat-log">
      <div class="bot message">
        Welcome to Ably's Realtime Pizza - the most responsive pizza this side
        of the sun! How may we assist you today?
      </div>
    </div>

    <div id="user-input">
      <input id="input-field" type="text" placeholder="Type your message here...">
    </div>
  </div>

  <!-- We include our application script at the end, to ensure the HTML above is
       already loaded and ready to interface with before the script runs: -->
  <script src="app.js"></script>
</body>
</html>

(b) Getting a basic application script working

The application script has a few parts to it, but isn’t too complex. The script will need to update the user interface at various points, and because the layout is fairly simple, we’re just going to use vanilla JavaScript code to accomplish this, rather than over-complicating things with frameworks such as Angular or React. For anything more ambitious, you’ll likely want to upgrade the code in this tutorial to use a framework of your choosing.

The first thing to do is to instantiate the Ably client library (already linked for you in the above HTML) and verify that we can send and receive messages successfully. The following code will define a channel, subscribe to it, and use it to publish customer messages. If everything is working, the fact that we’re subscribed to the same channel that we’re publishing to means that our messages will simply be echoed back to us via the receiveMessage handler function, which will then append them to the chat log in the HTML.

Don’t forget to replace YOUR_API_KEY_GOES_HERE with your actual API key!

// This Ably client library is available due to the script in the HTML header:
const ably = new Ably.Realtime('YOUR_API_KEY_GOES_HERE');

// Remember when creating your API key that you specified that it would only
// work for channels starting with the string "pizza:". When testing the script,
// try changing the channel name to something else and see how Ably emits an
// error to your browser developer console.
const channel = ably.channels.get('pizza:messages');
channel.subscribe(receiveMessage);

// Define the listener function used above, and have it add the received message
// to the chat log in the user interface:
function receiveMessage(message) {
  // The appendMessageElement function is defined further below.
  appendMessageElement('bot', message.data.message);
}

// Retrieve references to the message input field and the chat log container:
const inputField = document.getElementById('input-field');
const chatLog = document.getElementById('chat-log');

// Define an event listener function to be triggered when a key is pressed:
function processInput(e) {
  if (e.which !== 13) return; // If it's not the ENTER key, don't do anything

  const message = inputField.value.trim(); // Grab the trimmed input message
  if (message.length > 0) { // Don't post blank messages

    // Asynchronously post the message to the Ably channel. The channel
    // subscription we created above will echo the message shortly:
    channel.publish('user', { message });
  }

  // Clear the input field, ready for the next message:
  inputField.value = '';
}

// Attach the above listener function to the message input field:
inputField.addEventListener('keydown', processInput);

// And finally, this function will append messages to the chat log. The first
// argument indicates whether the message comes from the user or the bot:
function appendMessageElement(type, message) {
  const div = document.createElement('div');

  // Add two CSS classes, because later we'll want to be able to visually
  // differentiate user messages from bot messages:
  div.classList.add(type, 'message');
  div.textContent = message;
  chatLog.appendChild(div);

  // If the conversation goes on for a while, messages will fall off the bottom
  // of the page. Instead, let's scroll to the bottom of the page automatically:
  const el = document.scrollingElement;
  el.scrollTop = el.scrollHeight;
}

With the above in place, fire up your browser and give it a go. Enter chat messages into the input box, press ENTER, and if all is well, they should be echoed back to you in less than a second. Try submitting a few messages one after the other.

If you’re not seeing your messages in the window, open your browser’s developer tools and check the console to see if there are any error messages being emitted by your script.

(c) Filling out the primary functionality

Now that you’ve verified that you’re able to connect to Ably and send and receive messages, it’s time to update the code to integrate it with everything you did earlier in this tutorial.

First, we’ll need two channels rather than one. The first channel is the outbound channel, and is where the webhook will be listening for messages to forward on to your Azure function. The second channel is the inbound channel where the bot’s response messages will be posted, and is the channel to which your script will attach a listener.

If two different customers are trying to order a pizza at the same time, we don’t want their messages getting mixed in together. To address this, we’ll be appending a random customer identification value to the inbound and outbound channel names, with the effect being that every customer is allocated their own unique pair of inbound and outbound channels. The customer ID will also need to be included in outbound messages so that your Azure function can figure out where to send responses. The channels will only be used for the duration of the chat session, which means the maximum number of active channels at any one time will be equal to twice the number of customers currently placing pizza orders. See the pricing page for details about the peak connection limit available for your Ably account.

Here’s some code to generate a random customer ID. This will be run when the page first loads, and you’ll be able to then use the customerID in your channel names:

// Generate a customer id, being sure to sandbox the setup code inside a closure
const customerId = (function () {

  // The full set of characters that can be included in a customer ID
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

  // An arrow function to a random character from `chars`
  const randomChar = () => chars[Math.floor(Math.random() * chars.length)];

  // Change this if you want a shorter or longer ID
  const idLength = 16;

  // Prepare an array of our chosen customer ID size
  const id = new Array(idLength);

  // Populate the array with random characters
  for (let i = 0; i < idLength; i++) {
    id[i] = randomChar();
  }

  // Join the array entries together to form the customer ID string
  return id.join('');
})();

Split your channel initialization code to generate two channels instead of one:

// The customer's messages will be posted here
const outboundChannel = ably.channels.get('pizza:customer:' + customerId);

// Messages from the bot will be posted here, where we'll be listening for responses
const inboundChannel = ably.channels.get('pizza:bot:' + customerId);

// Update your subscription to listen to the inbound channel
inboundChannel.subscribe(receiveMessage);

We’re not using any special server-side persistence features, so we’ll need to keep track of the conversation context and order details ourselves, and pass them to the chat bot as the conversation progresses. Only the bot will be updating the context; we’ll just keep track of it on the bot’s behalf, so that it isn’t lost from one message to the next.

let context = null; // We start with an uninitialized conversation context

function receiveMessage(message) {
  context = message.data.context; // The bot returns an updated context
  appendMessageElement('bot', message.data.message);
}

Let’s refactor the message publishing code into its own function. We’ll also modify what we’re sending so that it includes the customer ID in the message data. This will make it easy for the Azure function to reply on the correct channel without having to extract the customer ID from the name of the inbound channel:

function postMessage(message) {
  // Note the additional call to appendMessageElement, which previously was only
  // being used by the receiveMessage handler. Because we want to visually
  // distinguish user messages from bot messages, the first argument is 'user'
  // for outbound messages, and 'bot' for inbound messages. These are simply the
  // names of the CSS classes to be attached to the message div element appended
  // to the DOM.
  appendMessageElement('user', inputField.value.trim());

  // Post to the outbound channel, and wrap both the message and the customer ID
  // in a plain object that the webhook  will include when forwarding the data
  // on to your Azure function.
  outboundChannel.publish('user', { user: customerId, message });
}

Don’t forget to also update your processInput function to use postMessage rather than publishing directly:

function processInput(e) {
  if (e.which !== 13) { // Character code 13 is the ENTER key
    return;
  }

  const message = inputField.value.trim();
  if (message.length > 0) {
    postMessage(message);
  }

  inputField.value = '';
}

Once you’ve made the above changes to your application script, it’s time to test the app and have a conversation with your bot!

(d) Polishing up the experience

You may have noticed a short delay between your message submission and the bot’s response. It’d be good if we could get some visual feedback that they’re going to respond, so we’ll add a brief message that pretends they’re another person typing a response.

Also, the UI is not very useful unless the text input box is focused at all times, so we’ll add some additional event listeners to make sure that no matter where the user clicks on the page, or whether they leave or return to the browser window, the input box always automatically refocuses itself.

Modify the appendMessage function to return a reference to the HTML element it has just appended to the DOM:

function appendMessageElement(type, message) {
  const div = document.createElement('div');
  div.classList.add(type, 'message');
  div.textContent = message;
  chatLog.appendChild(div);

  const el = document.scrollingElement;
  el.scrollTop = el.scrollHeight;

  return div; // Add this line
}

Next, we’ll define a special “waiting” message that we add when the user types a message, and remove when a reply is received:

let waitingElement; // persist a reference to the HTML element

function setWaiting(isWaiting) { // isWaiting should be true or false
  if (isWaiting) {
    // If the waiting element already exists, remove it and re-add it so that it
    // is always the last message in the chat log:
    if (waitingElement) {
      waitingElement.remove();
    }
    // Note the "waiting" class which, if you're using the CSS provided in the
    // repository for this tutorial, will style the message so that it pulses
    // to indicate that something is happening in the background:
    waitingElement = appendMessageElement('waiting', 'typing ...');
  }
  else {
    // We'll call setWaiting(false) when the bot's response message is received,
    // which will trigger the following code to remove the waiting element:
    if (waitingElement) {
      waitingElement.remove();
      waitingElement = null;
    }
  }
}

Update the code to call the above function when sending and receiving messages:

function receiveMessage(message) {
  setWaiting(false); // The bot has replied, so remove the waiting message
  context = message.data.context;
  appendMessageElement('bot', message.data.data);
}

function postMessage(message) {
  appendMessageElement('user', inputField.value.trim());
  setWaiting(true); // Add a temporary waiting message
  outboundChannel.publish('user', { user: customerId, message });
}

Finally, at the end of the script where your text box event listener is defined, hook up a couple of extra event listeners to keep the user focused on the input text box:

function focusInputField() {
  inputField.focus();
}

// Re-focus the input field when the window regains focus:
window.addEventListener('focus', focusInputField);

// ... and if the user clicks anywhere else on the page:
document.body.addEventListener('click', focusInputField);

// When the page loads for the first time, focus the input field by default:
focusInputField();

Congratulations, you’re done!


Animated image of the browser app in action

Next Steps

1. Take a look at the Webhooks documentation for further details about what was described in this tutorial.
2. If you would like to find out more about Ably Integrations features and capabilities, see Ably Integrations.
3. Learn more about Ably features by stepping through our other Ably tutorials
4. Gain a good technical overview of how the Ably realtime platform works
5. Get in touch if you need help