Twilio Sync is a powerful tool that enables you to synchronize the state of your applications across platforms, with only milliseconds of delay. It's commonly used to establish chat services, power live dashboards for information like recent calls to a support agent, and integrates with Twilio Flex.
This guide will show how to combine Functions, Assets, and Sync into a web application that displays incoming text messages in real time. All without the need to run or maintain your own server 24/7.
index.html
file that users will access
To begin, follow the instructions below to create a Service and the first Function of this app.
In order to run any of the following examples, you will first need to create a Function into which you can paste the example code. You can create a Function using the Twilio Console or the Serverless Toolkit as explained below:
If you prefer a UI-driven approach, creating and deploying a Function can be done entirely using the Twilio Console and the following steps:
https://<service-name>-<random-characters>-<optional-domain-suffix>.twil.io/<function-path>
test-function-3548.twil.io/hello-world
.
Your Function is now ready to be invoked by HTTP requests, set as the webhook of a Twilio phone number, invoked by a Twilio Studio Run Function Widget, and more!
When a user visits the application, their browser will make a request to this Function for a Sync Access Token and the name of the Sync List the app will listen to. Name your first new Function access
, and paste in the contents of the code sample below.
This code generates a Sync Token using secured Environment Variables, and returns a stringified version of the token along with the name of the Sync List used by the application.
_30const AccessToken = Twilio.jwt.AccessToken;_30const SyncGrant = AccessToken.SyncGrant;_30_30exports.handler = (context, event, callback) => {_30 // Create a Sync Grant for a particular Sync service, or use the default one_30 const syncGrant = new SyncGrant({_30 serviceSid: context.TWILIO_SYNC_SERVICE_SID || 'default',_30 });_30_30 // Create an access token which we will sign and return to the client,_30 // containing the grant we just created_30 // Use environment variables via `context` to keep your credentials secure_30 const token = new AccessToken(_30 context.ACCOUNT_SID,_30 context.TWILIO_API_KEY,_30 context.TWILIO_API_SECRET,_30 { identity: 'example' }_30 );_30_30 token.addGrant(syncGrant);_30_30 // Return two pieces of information: the name of the sync list so it can_30 // be referenced by the client, and the JWT form of the access token_30 const response = {_30 syncListName: context.SYNC_LIST_NAME || 'serverless-sync-demo',_30 token: token.toJwt(),_30 };_30_30 return callback(null, response);_30};
The examples in this app leverage Environment Variables to share common strings, such as the Service SID and Sync List name, but the samples will work if you don't define your own.
However, you must define, at a minimum, an API Key and API Secret. Your Account SID should already be in your Environmental Variables by default, regardless of whether you're building in the Console or with the Serverless Toolkit.
The next important feature of this application is being able to push the contents of incoming texts to the Sync List, so they can render in real-time in the browser. To do this, create a new Function, and name it handle-sms
. Type or copy the contents of the following code sample into the handle-sms
Function, and save.
This Function works by leveraging the built-in Runtime.getSync method to bootstrap a Sync Client for you. It then verifies that the Sync List is available, appends the body of the incoming message (event.Body
) to the list, and returns an SMS to the sender acknowledging receipt of their text.
_44exports.handler = async (context, event, callback) => {_44 // Make sure the necessary Sync names are defined._44 const syncServiceSid = context.TWILIO_SYNC_SERVICE_SID || 'default';_44 const syncListName = context.SYNC_LIST_NAME || 'serverless-sync-demo';_44 // You can quickly access a Twilio Sync client via Runtime.getSync()_44 const syncClient = Runtime.getSync({ serviceName: syncServiceSid });_44 const twiml = new Twilio.twiml.MessagingResponse();_44_44 // Destructure the incoming text message and rename it to `message`_44 const { Body: message } = event;_44_44 try {_44 // Ensure that the Sync List exists before we try to add a new message to it_44 await getOrCreateResource(syncClient.lists, syncListName);_44 // Append the incoming message to the list_44 await syncClient.lists(syncListName).syncListItems.create({_44 data: {_44 message,_44 },_44 });_44 // Send a response back to the user to let them know the message was received_44 twiml.message('SMS received and added to the list! 🚀');_44 return callback(null, twiml);_44 } catch (error) {_44 // Persist the error to your logs so you can debug_44 console.error(error);_44 // Send a response back to the user to let them know something went wrong_44 twiml.message('Something went wrong with adding your message 😔');_44 return callback(null, twiml);_44 }_44};_44_44// Helper method to simplify getting a Sync resource (Document, List, or Map)_44// that handles the case where it may not exist yet._44const getOrCreateResource = async (resource, name, options = {}) => {_44 try {_44 // Does this resource (Sync Document, List, or Map) exist already? Return it_44 return await resource(name).fetch();_44 } catch (err) {_44 // It doesn't exist, create a new one with the given name and return it_44 options.uniqueName = name;_44 return resource.create(options);_44 }_44};
With the necessary Functions in place, it's time to create the front-end of this web application.
If you're following this example in the Twilio Console:
index.html
on your computer.
index.html
file, and save the file.
index.html
as a public Asset. You can do so by clicking
Add+
, selecting
Upload File
and finding
index.html
in the upload prompt, setting the visibility as
Public
, and then finally clicking
Upload
.
_73<!DOCTYPE html>_73<html lang="en">_73 <head>_73 <meta charset="UTF-8" />_73 <meta name="viewport" content="width=device-width, initial-scale=1.0" />_73 <meta http-equiv="X-UA-Compatible" content="ie=edge" />_73 <title>Runtime + Sync = 🚀!</title>_73 </head>_73 <body>_73 <main>_73 <h1>Ahoy there!</h1>_73 <p>This is an example of a simple web app hosted by Twilio Runtime.</p>_73 <p>_73 It fetches a Sync access token from a serverless Twilio Function,_73 renders any existing messages from a Sync List, and displays incoming_73 messages as you text them to your Twilio phone number._73 </p>_73 <h2>Messages:</h2>_73 <div id="loading-message">Loading Messages...</div>_73 <ul id="messages-list" />_73 </main>_73 <footer>_73 <p>_73 Made with 💖 by your friends at_73 <a href="https://www.twilio.com">Twilio</a>_73 </p>_73 </footer>_73 </body>_73 <script_73 type="text/javascript"_73 src="//media.twiliocdn.com/sdk/js/sync/v3.0/twilio-sync.min.js"_73 ></script>_73 <script>_73 window.addEventListener('load', async () => {_73 const messagesList = document.getElementById('messages-list');_73 const loadingMessage = document.getElementById('loading-message');_73_73 try {_73 // Get the Sync access token and list name from the serverless function_73 const { syncListName, token } = await fetch('/access').then((res) =>_73 res.json()_73 );_73 const syncClient = new Twilio.Sync.Client(token);_73 // Fetch a reference to the messages Sync List_73 const syncList = await syncClient.list(syncListName);_73 // Get the most recent messages (if any) in the List_73 const existingMessageItems = await syncList.getItems({ order: 'desc' });_73 // Hide the loading message_73 loadingMessage.style.display = 'none';_73 // Render any existing messages to the page, remember to reverse the order_73 // since they're fetched in descending order in this case_73 messagesList.innerHTML = existingMessageItems.items_73 .reverse()_73 .map((item) => `<li>${item.data.message}</li>`)_73 .join('');_73 // Add an event listener to the List so that incoming messages can_73 // be displayed in real-time_73 syncList.on('itemAdded', ({ item }) => {_73 console.log('Item added:', item);_73 // Add the new message to the list by adding a new <li> element_73 // containing the incoming message's text_73 const newListItem = document.createElement('li');_73 messagesList.appendChild(newListItem).innerText = item.data.message;_73 });_73 } catch (error) {_73 console.error(error);_73 loadingMessage.innerText = 'Unable to load messages 😭';_73 loadingMessage.style.color = 'red';_73 loadingMessage.style.fontWeight = 'bold';_73 }_73 });_73 </script>_73</html>
The magic here is primarily concentrated in the ul
element and accompanying JavaScript. Once the window finishes loading, the script requests the Sync List name and Access Token from the access Function. Once the script has that token, it uses that token with the twilio-sync library to create a local Sync Client. With that Sync Client, the script then gets the latest messages, injects them into the ul
as more list items, and sets up an event handler that appends new messages as soon as they come in.
Now is a good time to save and deploy this Service. Save all file changes, and click Deploy All if you're working from the Twilio Console, or run twilio serverless:deploy
from your project's CLI if you're following along with the Serverless Toolkit.
Once you have deployed your code, you could visit the web page, but, sadly, there will be no messages to show yet. We'll address that issue in the next section.
To complete this app, you will need to connect the handle-sms
Function to one of your Twilio Phone Numbers as a webhook. Follow the directions below, and configure handle-sms
as the webhook for incoming messages to your Twilio Phone Number of choice.
In order for your Function to react to incoming SMS and/or voice calls, it must be set as a webhook for your Twilio number. There are a variety of methods to set a Function as a webhook, as detailed below:
You can use the Twilio Console UI as a straightforward way of connecting your Function as a webhook:
ui
unless you have created
custom domains
), and finally
Function Path
of your Function from the respective dropdown menus.