Static Web app

This is a demo of using the Ably promise-based SDK in an Azure Static Web App.

It uses Token Authentication with a Token Request created by an Azure function managed by Azure static web apps and handed to the Ably-Realtime client to authenticate and keep your API key safe.

For the purposes of the demo, we request some TFL Tube Data and display the history.

Because we're using the Promises API, we can use async/await in our JavaScript code that runs in the browser.

Local dev pre-requirements

We'll use live-server to serve our static files and Azure functions for interactivity

npm install -g live-server
npm install -g azure-functions-core-tools

Set your API key for local dev:

cd api
func settings add ABLY_API_KEY Your-Ably-Api-Key

Running this command will encrypt your API key into the file /api/local.settings.json. You don't need to check it in to source control, and even if you do, it won't be usable on another machine.

How to run for local dev

Run the static web app:

npx live-server --proxy=/api:http://127.0.0.1:7071/api

And run the APIs

cd api
npm run start

How to run on Azure

  • Push this repository to GitHub
  • Create a new Azure Static Web App
  • Link it to your GitHub repository
  • Azure will add a build task to your repository and deploy the app

You'll need to add your API key into a a Configuration setting in the Azure management portal.

Click:

  • Click Configuration
  • Add a setting named ABLY_API_KEY with your API key as the value
  • Save the settings.

How it works

Azure static web apps don't run traditional "server side code", but if you include a directory with some Azure functions in your application, Azures deployment engine will automatically create and manage Azure functions for you, that you can call from your static application.

For local development, we'll just use the Azure functions SDK to replicate this, but for production, we can use static files (or files created by a static site generator of your choice) and Azure will serve them for us.

In the Azure function

We have a folder called API which contains an Azure functions JavaScript API. There's a bunch of files created by default (package.json, host.json etc) that you don't really need to worry about, and are created by the Functions SDK. If you wanted to expand the API, you would use npm install and the package.json file to manage dependencies for any addtional functions.

There's a directory api/createTokenRequest - this is where all our "server side" code lives.

Inside it, there are two files - index.js and function.json. The function.json file is the Functions binding code that the Azure portal uses for configuration, it's generated by the SDK and you don't need to pay attention to it. Our Ably code is inside the index.js file.

const Ably = require('ably/promises');

module.exports = async function (context, req) {
    const client = new Ably.Realtime(process.env.ABLY_API_KEY);
    const tokenRequestData = await client.auth.createTokenRequest({ clientId: 'ably-azure-static-site-demo' });    
    context.res = { 
        headers: { "content-type": "application/json" },
        body: JSON.stringify(tokenRequestData) 
    };    
};

This is all of our server side code. We're using the Ably JavaScript SDK, and exporting a default async function. The function.json file configures this to be our entrypoint, and the functions runtime. By default, configures this API to be available on https://azure-url/api/createTokenRequest

Using the client we created earlier, we create a token request.

The TokenRequest that is generated by this SDK call happens without calling the Ably servers, and allows the client side SDK to authenticate because the TokenRequest is signed with your API key on the server before it's sent to Ably.

The permissions applied to the temporary token Ably returns to your client are inherited from your API key, and configured in your Ably dashboard.

On the client side

For our UI, we have a single file in index.html.

There are a few things going on here, firstly, in the <head> we include the Ably SDK in a script tag from the Ably CDN.

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

In the <body> of the document, we create a <div> to put Ably responses into

<main>
  <h2>Oh hi, let me just grab some history from Ably!</h2>
  <div id="history" class="historyContainer"></div>
</main>

And finally, a <script> block to connect to, and use, the Ably API.

<script>
    (async function () {
    console.log("Oh hai! 🖤");
    
    const resultsDiv = document.getElementById("history");
    const ably = new Ably.Realtime.Promise({ authUrl: '/api/createTokenRequest' });
    const channelId = `[product:ably-tfl/tube]tube:northern:940GZZLUEUS:arrivals`;
    const channel = await ably.channels.get(channelId);
    await channel.attach();
    
    channel.subscribe(function(message) {
        console.log(message.data);
    }); 
    console.log("Subscribed");
    
    const resultPage = await channel.history({ untilAttach: true, limit: 1 }); 

    for (const item of resultPage.items) {
        const result = document.createElement("div");
        result.classList.add("item");
        result.innerHTML = JSON.stringify(item);
        resultsDiv.appendChild(result);
    }        

    })();
</script>  

This script block

  • Wraps our code in an async function so we can use async/await
  • Finds our div with the id history.

The body of the connect function is an example of using Ably to query TFL for northern line journies to Kings Cross, but for this particular example it's important to look at the line

const ably = new Ably.Realtime.Promise({ authUrl: '/api/createTokenRequest' });

Here we configure our Ably SDK to

  • Use the Promise client (which supports async/await)
  • Pass a configuration object with an authUrl.

When the client is created, it'll Azure function createTokenRequest, and round-trip to Ably to get a short-lived authentication token for subsequent API calls, renewing it's token as required automatically.

Conclusion

Azure Static Web Apps are a pretty cool and cheap way to host content on the internet. They're more or less configuration-less, intergrate well with GitHub actions, and allow you to add bits of server-side code without the cost of running a webserver.

In this example, we've looked at how we can use the "serverless" Azure functions, embedded in our static app, to protect our sensitive API key, while still allowing it to be used on the client side with little to no ceremony.

You could use this similar approach with any other secrets you want to make use of in the front-end.