The way new technology is best adopted is when it is paired with existing technology. Partnering with a known quantity makes the new thing so much more approachable to users. So, in that spirit, let’s marry Web 2.0 tech with Web 3.0 tech in this tutorial.

For this tutorial, you’re going to be making use of two third-party services: SimpleID and Twilio.

We’re not going to be building a front-end for this application. Instead, we’ll set up a NodeJS server (which is recommended when using SimpleID and any other service in which you need to protect API Keys and secrets). This server-side app is going to allow users to create accounts, log in to those accounts, and post messages. But here’s the kicker: When each new message is posted, you’ll receive a text alert with the content of the message. You’ll also be able to query for messages posted.

This type of functionality can be used in a variety of ways. I can think of a forum as one example, comments on a blog as another, and so many others. How you ultimately use the functionality is up to you, but by the end of this tutorial, you’ll have a server that will do the following:

  • Accept GET Requests
  • Accept POST Requests
  • Validate Requests
  • Create user sessions
  • Post data to IPFS tied to a specific user
  • Fetch data from IPFS
  • Send text alerts

Here’s what you’re going to need to be able to follow along with this tutorial:

  • Node installed
  • NPM installed
  • Text Editor

Before diving in, let’s plan out this project a little more. We are going to need users to be able to post raw data (comments, text, whatever) that is associated with them but pooled with everyone else. We are going to need to be able to fetch all of this data. We are also going to need to send a text message to you, the developer, for each piece of content posted.

This isn’t terribly fancy. No threaded content. Just a raw feed.

Let’s get started. You’ll need to sign up for a SimpleID account, which you can do here, and a Twilio account, which you can do here. Let’s walk through what’s needed for SimpleID first.

When you sign up for an account, you’ll be asked to verify your email address. Go ahead and do that. Once verified, you can create a project. Give it a name and a url where you might host this project. It will need to be an https url. This is a security restriction SimpleID has in place. When the project is created, go to your Modules page and select Ethereum for your authentication module and Pinata for your Storage module. Save your selections then return to the Account page. There, you can click on the View Project button to get the two items you’ll need for your project: apiKey and devId.

Now that that’s done, let’s get set up with Twilio. When you sign up, you’ll also need to verify your email with them. Do that and then you’ll need to verify your phone number for SMS. You should receive a verification text when you do this. Enter the code in the box on the Twilio screen. Now, Twilio is going to try to customize your onboarding. Go ahead and answer the questions, but we’ll get you where you need to go no matter how you answer. When you’re done with this process, you’ll land on your dashboard where you can get a Trial Number. Do that because it’s free. Record the number somewhere for now. You can also now record the Account SID and Auth Token.

Now, let’s build some stuff!

Go ahead and create a folder wherever you like to keep your development projects:

mkdir text-comments && cd text-comments

Within that folder, let’s initialize our project by running npm init. You can accept all the defaults as this process runs. Once it’s done, we need to install some dependencies.

npm i express simpleid-node-sdk twilio body-parser

With that command, which might take a moment to run, you’ll be installing the three dependencies we need for this project:

  • ExpressJS — for our server
  • SimpleID — for auth and IPFS storage
  • Twilio — for the texting
  • Body-Parser — for parsing json requests easily

When everything is done installing, let’s create an index.js file. You can do this right from the command line with touch index.js. Then, open your project in your favorite text editor.

We’re going to need to get some boilerplate set up for our Express server. So add this to your index.js file:const express = require('express');
const app = express();
const port = 3000;
const bodyParser = require("body-parser");app.use(bodyParser.json());app.get('/content', (req, res) => {
 //this is where we will fetch the IPFS content
 res.send('Eventually content will be here')
})'/auth/create', async (req, res) => {
 //this is where we will create a user account
 res.send("Account Creation Here");
})'/auth/login', async (req, res) => {
 //this is where we will log a user in
 res.send("Log in Here");
})'/postContent', async (req, res) => {
 //this is where we will post the IPFS content
 res.send("IPFS Content Posted Here");
})'/sendText', async (req, res) => {
 //this is where we will trigger the outbound text
 res.send("Text sent here");
})app.listen(port, () => console.log(`Example app listening on port ${port}!`))

With that code, we can now test our server code by making some API calls. Let’s make it easy to start up our server by opening the package.json file and adding this in the scripts section:"start": "node index.js",

With that, we can now run npm start from the command line to start our server. Give it a shot and you should see the command line print out:Example app listening on port 3000!

You now have a working API you can test. You can use Postman or the command line to test this depending on what you’re comfortable with. I’ll be using cURL scripts to keep things simple. So, open a new tab or window in your terminal and run this:curl -X GET \

You should get back the response Eventually content will be here. Nice! Now try the post requests:curl -X POST \
 http://localhost:3000/auth/createcurl -X POST \
 http://localhost:3000/auth/logincurl -X POST \
 http://localhost:3000/postContentcurl -X POST \

When you run each of those, you should get back the responses we typed out as placeholder. If that worked, we’re ready to start building this for real. We’re going to be taking material right from the SimpleID and Twilio docs to help us here. So, starting with the SimpleID configuration, we need to add this to the top of our index.js file:const simple = require('simpleid-node-sdk');
const config = {
 apiKey: ${yourApiKey}, //found in your SimpleID account page
 devId: ${yourDevId}, //found in your SimpleID account page
 authProviders: ['ethereum'], //array of auth providers that matches your modules selected
 storageProviders: ['pinata'], //array of storage providers that match the modules you selected
 appOrigin: "", //even if using SimpleID on a server or as a desktop/mobile app, you'll need to pass an origin for reference
 scopes: ['publish_data', 'store_write'], //array of permission you are requesting from the user
 development: false

With this configuration, you are ready to create an account for your users (just make sure you actually fill in the configuration with your own information). At the /auth/create endpoint you previously created, we are going to take a payload of username, email, password. We’re going to then combine that with our config settings to create a decentralized identifier for the user (we’ll return an ethereum address for them). With that, we will then be able to log the user in (note: creating an account automatically logs the user in and will return a user session object).

In the /auth/create endpoint, let’s replace the placeholder response and add the'/auth/create', async (req, res) => {
 const { email, id, password } = req.body;
 const credObj = {
   hubUrl: "" //this is for blockstack storage, but needs to be sent even when not in use
 const account = await simple.createUserAccount(credObj, config);

This comes straight from the SimpleID docs. We are taking the user credentials, combining them with our configuration object, and creating a user account. Let’s test this by sending a post request. First, we need to kill our server and restart it so it’s aware of our changes. Let’s fix that because this will get real annoying fast as we make changes to our code.

After you’ve killed the server (`ctrl + c or cmd+c), install nodemon like this: npm i -g nodemon. Then all we need to do is update our start script in package.json:...
"scripts": {
 "start": "nodemon index.js"

Now, run npm start and we shouldn’t have to constantly kill and restart the server!

Let’s test our endpoint with some JSON data passed to it:curl -X POST \
 http://localhost:3000/auth/create \
 -H 'Content-Type: application/json' \
 -d '{
"id": ${uniqueUserName},
"email": ${anyEmailAddress},
"password": ${somePassword}

If you fill in the uniqueUserName, anyEmailAddress, and somePassword with your own values, you should now be able to get a response from your server. If you chose a username that aready exists, you’ll get a response like this:{
 "message": "name taken",
 "body": null

Otherwise, the process will work all the way through to login and return the user session object like this:{
 "message": "user session created",
 "body": {
   "appConfig": {
   "appDomain": "",...

I truncated the response in my example because it’s too long for the sake of the tutorial. But congratulations! If you got a response like this, your user is now logged in.

But, what if we want that user to log in again? We don’t want them to create a new account. Let’s update our /auth/login endpoint'/auth/login', async (req, res) => {
 const { id, password } = req.body;
 const credObj = {
   hubUrl: ""
 const params = {
   appObj: config
 }  const loggedIn = await simple.login(params);  res.send(loggedIn);

Here, we are taking a post of JSON data that includes the user’s id and password. We are combining that in a credentials object and merging our config object into one parameters object. We send that to the login function to get our response. Let’s try it:curl -X POST \
 http://localhost:3000/auth/login \
 -H 'Content-Type: application/json' \
 -d '{
"id": "from_node_server_000",
"password": "super secure password"

If successful, you’ll see the same response you saw at the end of a successful account creation post. The message will be: user session created.

Ok, now we have authentication working which means we can start posting data to IPFS and associate it with the logged in user. Let’s start by setting up our /postContent endpoint. Because we’re not building a front-end for this app, there are a couple of approaches we could take. We could send a payload on login or account creation that includes the user credentials as well as the content to be posted. Or, we could take the response from account creation or log in, hold onto it somewhere and use it to tie a user to a post.

I think when an application like this is wired up to the front-end, the second option makes the most sense. So, let’s do that. In our /postContent endpoint, replace the placeholder with'/postContent', async (req, res) => {
 const { id, ethAddr, content } = req.body;
 const contentToPin = {
   address: ethAddr,
 }  const params = {
   devId: config.devId, //your dev ID found in your SimpleID account page
   username: id, //your logged in user's username
   id: "ipfs-text", //an identifier you can use to reference your content later
   content: contentToPin, //the content we discussed previously
   apiKey: config.apiKey, //the api key found in your SimpleID account page
 }  const postedContent = await simple.pinContent(params);  res.send(postedContent);

All we’ve done here is take the user’s id (you can grab this from any of the accounts you created in testing) and the user’s Ethereum address. The Ethereum address is returned in the user session object and can be access like this://If logging in creating a new account

Let’s try to post some content:curl -X POST \
 http://localhost:3000/postContent \
 -H 'Content-Type: application/json' \
 -H 'Host: localhost:3000' \
 -d '{
"id": ${username},
"ethAddr": ${ethereumAddress},
"content": "This is the first post. What do you think?"

If that’s successful, you’ll get a response that indicates success and includes an IPFS hash. It should look like this:{
 "message": "content successfully pinned",
 "body": "QmNZqBFvZq24GuP9H8B3ae1YXAHd8VY8H56PwcWQhrs5Kc"

We can go check this content out on IPFS now. Since we use Pinata for pinning IPFS content (to ensure availability), we’ll use their IPFS gateway to fetch this example. You can see it here.

Pretty cool! Now, we need to send a text alert to you, the developer about the new content being posted. Enter Twilio.

At the top of your index.js file, add the following:const accountSid = ${yourAccountSID};
const authToken = ${yourAuthToken};
const client = require('twilio')(accountSid, authToken);

Your accountSid and your authToken were generated earlier when you created your Twilio account. Just plug those in here and then we can focus on the /sendText endpoint. We need to take the content of a post and send it via text. Or send some version of it via text at least. Let’s fill out the endpoint code and then we can think about how we’ll post to that endpoint. Replace your placeholder code in the /sendText endpoint'/sendText', async (req, res) => {
 const { content, from } = req.body;  client.messages
   body: `New post from ${from}: ${content}`,
 from: ${yourTwilioNumber,
 to: ${yourActualNumber
 .then(message => res.send(message));

So, this endpoint is now expecting a JSON payload that include the content of the post and the person it’s from. That information is then sent as the body of the Twilio text. You’ll need to grab that phone number you got as part of the free Twilio trial and plug it into the from field. And assuming you want to be the one to receive these messages, enter your phone number in the to field.

The question now is, how do we post to this endpoint? We want it to happen immediately after the IPFS content is posted. So, it would make sense to just call /sendText endpoint from in that endpoint’s response. But why even have a separate endpoint for /sendText? Couldn’t we just dump the Twilio code into the /postContent endpoint?

We could, but what if down the line we want to be able to send texts after some of the content has been manipulated in some way? Maybe on the client, there’s some interaction after the content is posted to IPFS and then we want to call the /sendText endpoint. By having it as a separate endpoint, we give ourselves flexibility, even if we won’t use that flexibility today.

To post to the /sendText endpoint from within the /postContent endpoint’s response, we’ll use the request module. We can install that by killing the server and running npm i request.

At the top of your index.js file, add:const request = require('request');

Now, inside of our /postContent endpoint, let’s update the code right below const postedContent = await simple.pinContent(params):...
const postData = {
 from: params.username,
 content: params.content.content
}var options = {
 method: 'POST',
 url: 'http://localhost:3000/sendText',
 headers: {
   Host: 'localhost:3000',
  'Content-Type': 'application/json' },
 body: postData,
 json: true };request(options, function (error, response, body) {if (error) throw new Error(error);

Let’s try this now. If all goes well, content will be posted to IPFS and then we’ll receive a text. Here’s the curl command to post to IPFS again:curl -X POST \
 http://localhost:3000/postContent \
 -H 'Content-Type: application/json' \
 -H 'Host: localhost:3000' \
 -d '{
"id": ${username},
"ethAddr": ${ethereumAddress},
"content": "This is the first post. What do you think?"

You can change the content if you’d like. I’m leaving mine for simplicity. Give it a shot and you should receive a text that looks like this:

Awesome! We just built a *very* basic notification system using a combination of web2 and web3 technology. One last thing we want to do is list all the posts for a given user. To do this, we can use the previously created GET endpoint /content. Find that in your index.js file and replace the placeholder with:app.get('/content', async (req, res) => {
 const username = req.query.username;
 const params = {
   devId: config.devId, //your dev ID found in your SimpleID account page
   username: ${username}, //you logged in user's username
   id: "ipfs-text", //the identifier you used for reference of the pinned content
   apiKey: config.apiKey //the api key found in your SimpleID account page
 }  const fetchedContent = await simple.fetchPinnedContent(params);

Make sure to pass in the username from whom you’d like to fetch posts, fill in your other parameters, and then let’s give it a shot:curl -X GET \
 'http://localhost:3000/content?username=${username}' \

Notice that we’re passing in the username as a query string parameter. That’s being handled on the server with the line we added at the top of the /content endpoint:const username = req.query.username;

You should get back a response like:{
 "message": "Found pinned content",
 "body": "{\"id\":\"from_node_server_000\",\"date\":1567694211655,\"address\":\"0x91702078DeA9D1d9354467F58E0225AD2C8445Ab\",\"content\":\"This is the first post. What do you think?\"}"

That’s it! We just built a server-side application that supports, Ethereum authentication, IPFS content storage and retreival, and text messaging. If you’d like to see the full source code, that can be found here.

With your newfound powers, you can now wire this up to a front-end application and put it to use creatively. There are enhancements you could and should ake to the server code as well. Right now, your server routes are unprotected. Anyone can make requests to them. You might want to throttle based on IP Address to help avoid DOS attacks. You might want to have some sort of bearer token you look for. There are a lot of things you can do to improve your code from here. But enjoy the fact that you built a sneakily complex app with very little actual complexity in its implementation.