Create a Real-Time Note Collaboration Tool using Twilio Sync, Node.js, and Express.js

June 09, 2021
Written by
Reviewed by
Diane Phan
Twilion

Create a Real-Time Note Collaboration Tool using Twilio Sync, Node.js, and Express.js

One of the brightest spots in the future of communication-based apps is the ability to create real-time communication between various devices. Document apps such as Google Docs, Notion, and even iOS Notes are made possible due to the ability to synchronize state and data in real-time.

Twilio Sync makes this same functionality possible by offering two-way, real-time communication between browsers, mobile devices, and the cloud.

In this tutorial, we're going to use this state synchronization service to create a real-time note collaboration tool using Twilio Sync. When completed, all users will be able to co-browse the same notepad and type a document together in real-time.

If you’re still not clear on what Sync does, consider that I wrote this tutorial in Notion. Their synchronization service allowed me to seamlessly switch between my phone and their desktop app without revision conflict. This is the power of real-time synchronization!

NOTE: Twilio Sync focuses on state management in the cloud and providing quick updates, it is not ideal to use Twilio Sync as a primary database to store large quantities of content.

To learn more about the best use cases and how synchronized data can provide a personalized experience for your app, check out the Twilio Sync Overview.

Screenshot of the completed notepad

Tools needed to get started

This project will be created with Node.js, Express.js, and Twilio Sync. To get started, have the following services ready to use:

These are all the prerequisites we’ll need to get started. Be sure to save all of the credentials created, saved to a safe location.

Create a new package.json file

This tutorial will create a Node.js server so that the final app can be coupled into one code base for simplicity.

Create a new project by running the following commands from the command line wherever you keep your projects:

mkdir note-collab
cd note-collab
npm init -y

Create a new Express.js app

Right now our application folder is empty aside from the package.json file. We want to change that by creating a simple HTML page that will display a <textarea> to co-browse.

Express.js is a minimal Node.js web application framework that will allow us to combine our front end form with routes to generate tokens in the back end.

Add Express to your project with the following command:

npm install express

Create an index.js file

Now that all of our dependencies have been created, we need to create an index.js file to serve our app. Open your IDE and create a new index.js file inside of the project directory.

Create an index.html file

We will need to create an index.html file to display the notepad. Express allows us to serve static HTML files using the sendFile() function.

As a nod towards organization, create a new public folder inside of the project directory. This folder will store the index.html file.

mkdir public

Create a new file called index.html inside of the project directory and open it in your IDE.

Add the following code to the file:

<html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
        <script type="text/javascript" src="//media.twiliocdn.com/sdk/js/sync/v0.12/twilio-sync.min.js"></script>
    </head>
    <body class="bg-grey-100 p-10 flex">
        <textarea id="notepad" class="h-44 w-full shadow-lg border rounded-md p-3 sm:mx-auto sm:w-1/2"></textarea>
    </body>
</html>

Our notepad HTML consists of:

  • The Tailwind CSS framework loaded via CDN. This will allow us to easily style the notepad. Note that the CDN is not recommended for production.
  • The Twilio Sync JavaScript client. This library will allow us to monitor our state and any changes made to the Sync document.
  • A <textarea> is styled to function as the collaborative notepad.

Create an endpoint to display the index.html file

Setup the Express.js app to create an endpoint at the root path to return the index.html file.

Add the following code to the index.js file:

const express = require('express');
const path = require('path');
const app = express();
const port = 3000;

// Return index.html at the root route
app.get('/', (req, res) => {
    res.sendFile(path.join(__dirname, '/public/index.html'));
});

// Start the node.js server
app.listen(port, () => {
    console.log(`Twilio Sync Co-browsing App listening at <http://localhost>:${port}`);
});

In your command prompt or terminal, run the following code to start your local server:

node index.js

Open your browser at http://localhost:3000 and you should see a similar screen.

Empty notepad ready for typing

We're now ready to learn about Twilio Sync by creating an access token and later using the Twilio Sync JavaScript Client.

Install .dotenv to securely store Twilio Credentials

Earlier in the tutorial you set up new credentials so that all requests can be securely authenticated. In order to access those credentials within this app they'll need to be securely stored.

We'll use a .env (dotenv) file to handle this. To allow our app to read the .env file we'll need to add the dotenv package. Stop your server by pressing CTRL + C or open a new terminal window and navigate back to your project’s directory. Then, run the following command to install the package:

npm install dotenv

Create a .env file at the root of the project folder and add your Twilio Credentials:

TWILIO_ACCOUNT_SID=
TWILIO_SYNC_SERVICE=
TWILIO_API_KEY=
TWILIO_API_SECRET=

Add the dotenv requirement to the top of the index.js file.

require('dotenv').config();

Download the Twilio Node.js Helper library

Before we reference our credentials, we'll need to actually install the Twilio Node.js Helper Library. Run the following command from inside of your project directory in the command line:

npm install twilio

Create an access token

Our code will use the credentials to authenticate each request in creating an access token.

After the access token is created, we will use it to authenticate the Sync service.

Define the following variables after the const port = 3000 declaration:

const AccessToken = require('twilio').jwt.AccessToken;
const SyncGrant = AccessToken.SyncGrant

Now create a new endpoint GET /token endpoint that will allow us to request a new access token on page load.

app.get('/token', (req, res) => {
        
    // For demonstration purposes, each session has the same identity
    const identity = "contributor"

    // Create a "grant" identifying the Sync service instance for this app.
    syncGrant = new SyncGrant({
        serviceSid: process.env.TWILIO_SYNC_SERVICE_SID,
    });

    // Create an access token which we will sign and return to the client,
    // containing the grant we just created and specifying his identity.
    const token = new AccessToken(
        process.env.TWILIO_ACCOUNT_SID,
        process.env.TWILIO_API_KEY,
        process.env.TWILIO_API_SECRET,
    );

    token.addGrant(syncGrant);
    token.identity = identity;

    // Serialize the token to a JWT string and include it in a JSON response
    res.send({
        identity: identity,
        token: token.toJwt()
    });
});

Start the server and visit http://localhost:3000/token to view a sample access token.

node index.js

We're nearing the end of completing our app. At this point we can type in the notepad and generate an access token. The best part follows: adding a listener to synchronize the data in real time!

Add a listener for when we type in the notepad

We want to be able to type in the notepad in two separate browsers and see them update in real time. To do this, we're going to add an event listener to the <textarea> that detects when we've finished a word (space, return, or punctuation mark) to trigger synchronization.

We will be using a Sync Document to store the most recently updated version of the notepad contents. Twilio Sync allows you to create three other types of Sync objects for various use cases.

We've chosen to use a Document because they're a great introduction to Twilio Sync as it provides a basic publish/subscribe transaction where history synchronization isn't a requirement.

Add the following code to the bottom of the public/index.html file.

<script>
    let notepad = document.getElementById('notepad')
    let twilioSyncClient;

    fetch('/token')
        .then(response => response.json())
        .then(data => { 
            
            let token = data.token
            let syncClient = new Twilio.Sync.Client(token)

            twilioSyncClient = syncClient
            
            twilioSyncClient.document('notepad')
                .then(function(document) {
                    
                    // Load the existing Document
                    notepad.value = document.value.content

                    // Listen to updates on the Document
                    document.on('updated', function(event) {
                        
                        // Update the cursor position
                        let cursorStartPos = notepad.selectionStart;
                        let cursorEndPos = notepad.selectionEnd;
                        
                        // Set the notepad content to the synced Document
                        notepad.value = event.value.content

                        // Reset the cursor position
                        notepad.selectionEnd = cursorEndPos

                        console.log('Received Document update event. New value:', event.value);
                    })
                })
                .catch(function(error) {
                    console.error('Unexpected error', error)
                });
        })
    
    const syncNotepad = (syncClient) => {
    
        let notepadContent = document.getElementById('notepad').value

        twilioSyncClient.document('notepad').then(function(doc) {
            doc.update({ content: notepadContent });
        })
        
    }

    // Add listener 
    document.getElementById('notepad').addEventListener("keyup", (event) => {
        
        // Define array of triggers to sync (space, enter, and punctuation)
        // Otherwise sync will fire every time 
        const syncKeys = [32,13,8,188,190]

        if(syncKeys.includes(event.keyCode)) {
            syncNotepad(twilioSyncClient)
        }
    })
</script>

Each Sync Document has a uniqueName; an application-defined string that uniquely identifies the resource. Our Document will be called notepad.

When our page loads, a new access token is generated and the first request is made to open the notepad document. After it loads, the existing document is loaded into the <textarea> content.

Most importantly, a listener is initialized that will synchronize the cursor position and set the notepad content to the synced Document.

All of the aforementioned steps are triggered every time time the keyup event fires on the <textarea>.

Testing

That's it! We're now ready to test the app. Open http://localhost:3000 in two browsers, side-by-side. Type in one and watch it update in the other.

Screen recorded example of the note collaboration tool

 

Conclusion

Here's to a great introduction on Twilio Sync! You've successfully created a real-time, note collaboration app. This app could be extended by occasionally saving the content into a database or even adding a full rich text editor for greater customization. The cursor positions could also be communicated to each user so that they know where the other person is typing and we could also communicate whether or not the other users are typing. The possibilities for customizing the collaboration experience are almost endless.

In a future version of this tutorial, we'll add Twilio Programmable Video to create a video collaboration authoring tool.

Marcus Battle leads an awesome team of engineers writing technical content for Twilio called Developer Voices. He can be reached via: