Hacking Graphite: Create Your Own Personal API

Justin Hunter
The Lead
Published in
14 min readOct 9, 2018

--

Hacking Graphite is a series of tutorials that harnesses the power of decentralization, open source code, and creativity. Come hack with Graphite and learn JavaScript, Blockstack, and decentralization in the process.

This is the first tutorial in the Hacking Graphite series. The first thing to know is what Graphite is. Check it out for yourself:

https://app.graphitedocs.com

Graphite is a decentralized and encrypted alternative to Google’s G-Suite. Think, Google Docs, Sheets, Drive, etc. Graphite is built using Blockstack’s open source SDKs. And because Graphite is also open source, we’re going to use Graphite’s existing code to hack together new solutions and learn about JavaScript, React, Encryption, Decentralization, and more along the way.

So, what are we actually going to be building here? We’re going to take the existing Spreadsheet component from Graphite and use it to build a back-end API that can be used to power just about anything you want. Graphite Sheets is the least developed part of Graphite thus far (maybe making it better will be a future tutorial), but there’s plenty built now to achieve this goal.

Note: The API we build will be read-only, but it will allow direct updates from the Spreadsheet you use.

Ready? Let’s do it!

The first thing we’re going to do is sign into Graphite. Why? Two reasons:

  1. You’ll need a valid Blockstack username to be able to access the course forum (which itself is a Graphite Doc)
  2. To get a feel for how Graphite works before you hack it up.

Sign in here. If you don’t have a username through Blockstack yet, create one. It only takes a minute.

Ok, all set? Great. Explore Graphite for a minute. Create a document. Create a spreadsheet. Upload a file. Feeling comfortable? Let’s get started.

Getting Started

Graphite is written in JavaScript using React, NPM, and Webpack among other dependencies. Some familiarity in those four things will be necessary. You’ll also need some experience with the command line since that’s how we will be cloning Graphite and installing dependencies.

Since we’ll be developing locally, and since Graphite’s authentication method through Blockstack requires Cross-Origin Resource Sharing to be disabled when developing locally, you’ll want to download a CORS extension for Firefox or Chrome. If you’re using Safari, you can just enable the “Develop” tab in your menu bar and then click “Disable Cross-Origin Resource Sharing Restrictions.”

Here’s the Firefox extension link.

Here’s the Chrome extension link.

You will need Node installed on your system. You can do that here. You will also need to have an up to date version of NPM installed. This comes with Node, so just make sure you’re running a recent version of Node.

Clone and Run

Step 1:

Fire up your terminal and let’s get started. Clone the repository and install dependencies:

git clone https://github.com/Graphite-Docs/graphite.gitor git clone https://github.com/Graphite-Docs/graphitecd graphite
npm install
npm run start

Oh no! An error, right? Something like: Module not found: Can't resolve './prod' in '...'

No, problem! This is simply because the production keys are never committed to GitHub. We won’t need those for this exercise, so the open the graphite folder in your favorite text editor. Open the src folder and then open the components folder. In there, you’ll see the foldered titled helpers, which according to our error message is where the problem is.

Take a look at the file keys.js. You’ll see that file is exporting a dev.js file if the app is running in any non-production environment. If it is running in production, it’s exporting the prod.js file. However, that file doesn’t exist in your locally created instance of Graphite. So, there are two solutions:

  1. Create an empty prod.js file in the helpers folder
  2. Comment our the export line used when the app is running in production

We’ll assume you someday want to create production keys for something. So let’s just create an empty file titled prod.js in the helpers folder. As soon as you do this, you should see the app refresh at http://localhost:3000.

Step 2:

Assuming all goes well, you’ll see the sign in page. Let’s make sure the app is running properly.

Turn on your CORS extension or click the “Disable Cross-Origin Restrictions” option in Safari. Then, sign into your local version of Graphite. If sign up goes, well, you’ll see the Graphite dashboard.

Great! Time to get hacking.

Design it, Script it, Test it

So far, we haven’t created anything new. We’ve just gotten you up to speed with a local instance of Graphite. But that’s about to change. Here, we can finally start adding new code.

First, in your src folder, go to components, then go to sheets. Find the file SingleSheet.js. That’s where we’ll be working. Remember, there’s a lot going on under the hood in Graphite, and Sheets, especially, is a work in progress. There’s going to be a lot of code here you won’t need to worry about, but we’ll try to find the right place to make your updates and keep you on track.

Let’s add a button that will eventually reveal our API endpoint for easy access. Scroll down until we find an if statement followed by a return statement.

The if statement looks like this:

if(this.state.initialLoad === “”)

It’s simply checking to see if the page is done loading so that we can display more than a blank screen if loading is ongoing. So, you’ll need to find the return statement corresponding to that if statement as well as the one corresponding to the else statement. Each of them will have html that looks something like this:

return (
<div className="center-align sheets-loader">
<div className="navbar-fixed toolbar">
<nav className="toolbar-nav">
<div className="nav-wrapper">
<a onClick={this.handleBack} className="left brand-logo"><i className="small-brand material-icons">arrow_back</i></a>
<ul className="left toolbar-menu">
<li><input className="white-text small-menu" type="text" placeholder="Sheet Title" value={this.state.title} onChange={this.handleTitleChange} /></li>
<li><a className="small-menu muted">{autoSave}</a></li>
</ul>
<ul className="right toolbar-menu small-toolbar-menu auto-save">
<li><a className="tooltipped dropdown-button" data-activates="dropdown2" data-position="bottom" data-delay="50" data-tooltip="Share"><i className="small-menu material-icons">people</i></a></li>
<li><a className="dropdown-button" data-activates="singleSheet"><i className="small-menu material-icons">more_vert</i></a></li>
<li><a className="small-menu tooltipped stealthy-logo" data-position="bottom" data-delay="50" data-tooltip="Stealthy Chat" onClick={() => this.setState({hideStealthy: !hideStealthy})}><img className="stealthylogo" src="https://www.stealthy.im/c475af8f31e17be88108057f30fa10f4.png" alt="open stealthy chat"/></a></li>
</ul>
...

To make things simple, we are going to add our API endpoint button in the header. Right now, your header should look like this:

There are only two icons set off to the far right. A people icon for sharing your sheet and a menu icon with some additional functions. We’ll add a new icon just to the left of the people icon for simplicity.

Graphite makes use of the Materialize framework. You can customize this however you’d like, use a different CSS framework, or roll your own, but that’s outside the scope of this tutorial. Here’s a link to Materialize’s documentation:

In our code, near the return statements by both the if and the else statements, you’ll see the nav bar html:

<nav className="toolbar-nav">
<div className="nav-wrapper">
<a onClick={this.handleBack} className="left brand-logo"><i className="small-brand material-icons">arrow_back</i></a>
<ul className="left toolbar-menu">
<li><input className="white-text small-menu" type="text" placeholder="Sheet Title" value={this.state.title} onChange={this.handleTitleChange} /></li>
<li><a className="small-menu muted">{autoSave}</a></li>
</ul>
<ul className="right toolbar-menu small-toolbar-menu auto-save">
<li><a className="tooltipped dropdown-button" data-activates="dropdown2" data-position="bottom" data-delay="50" data-tooltip="Share"><i className="small-menu material-icons">people</i></a></li>
<li><a className="dropdown-button" data-activates="singleSheet"><i className="small-menu material-icons">more_vert</i></a></li>
<li><a className="small-menu tooltipped stealthy-logo" data-position="bottom" data-delay="50" data-tooltip="Stealthy Chat" onClick={() => this.setState({hideStealthy: !hideStealthy})}><img className="stealthylogo" src="https://www.stealthy.im/c475af8f31e17be88108057f30fa10f4.png" alt="open stealthy chat"/></a></li>
</ul>
{/*Share Menu Dropdown*/}
<ul id="dropdown2"className="dropdown-content collection cointainer">
<li><span className="center-align">Select a contact to share with</span></li>
<a href="/contacts"><li><span className="muted blue-text center-align">Or add new contact</span></li></a>
<li className="divider" />
{contacts.slice(0).reverse().map(contact => {
return (
<li key={contact.contact}className="collection-item">
<a onClick={() => this.setState({ receiverID: contact.contact, confirmAdd: true })}>
<p>{contact.contact}</p>
</a>
</li>
)
})
}
</ul>
{/*Share Menu Dropdown*/}
{/* Dropdown menu content */}
<ul id="singleSheet" className="dropdown-content single-doc-dropdown-content">
<li><a onClick={() => this.setState({ remoteStorage: !remoteStorage })}>Remote Storage</a></li>
<li className="divider"></li>
<li><a onClick={this.print}>Print</a></li>
<li><CSVLink data={this.state.grid} filename={this.state.title + '.csv'} >Download</CSVLink></li>
{this.state.journalismUser === true ? <li><a onClick={() => this.setState({send: true})}>Submit Article</a></li> : <li className="hide"/>}
<li className="divider"></li>
<li><a data-activates="slide-out" className="menu-button-collapse button-collapse">Comments</a></li>
{this.state.enterpriseUser === true ? <li><a href="#!">Tag</a></li> : <li className="hide"/>}
{this.state.enterpriseUser === true ? <li><a href="#!">History</a></li> : <li className="hide"/>}
</ul>
{/* End dropdown menu content */}
{/*Remote storae widget*/}
<div className={remoteStorageActivator} id="remotestorage">
<div id='remote-storage-element-id'></div>
</div>
{/*Remote storae widget*/}
</div>
</nav>

If you’re comfortable and know what you’re doing, this is your chance to strip things down to the bare bones. Use this code however you want or simply add to it like this tutorial is instructing you to. It’s up to you. But, I will assume you’re adding to the code and that’s what the instructions will follow.

Here’s the header code with the new link list item:

<nav className="toolbar-nav">
<div className="nav-wrapper">
<a onClick={this.handleBack} className="left brand-logo"><i className="small-brand material-icons">arrow_back</i></a>
<ul className="left toolbar-menu">
<li><input className="white-text small-menu" type="text" placeholder="Sheet Title" value={this.state.title} onChange={this.handleTitleChange} /></li>
<li><a className="small-menu muted">{autoSave}</a></li>
</ul>
<ul className="right toolbar-menu small-toolbar-menu auto-save">
<li><a><i className="small-menu material-icons">link</i></a></li>
</ul>
</div>
</nav>

OK, now, let’s make it so that clicking that link icon creates a modal with the API endpoint listed. Materialize makes this very easy. Above the if/then statement that returns the html we’ve been using is the renderView function. This is simply a variable method that is used to ultimately render the content of this React component. We can add the following just below that method:

renderView() {
window.$('.modal').modal();
...

We add window ahead of the jQuery function since jQuery isn’t specifically imported into any component in Graphite’s code. Instead, it’s added as a script tag in the index.html file. So, we can make use of window to access jQuery’s functionality anywhere in Graphite’s code this way.

Now that the modal is initialized, let’s add the modal code. I’m going to add it right after the closing </nav> and </div>tags (You really only need to add this in the else statement’s section of renderView):

...
</nav>
</div>
<div id="apiModal" className="modal">
<div className="modal-content">
<h4>API Endpoint</h4>
<p>Here is your API endpoint URL:</p>
<a href={this.state.api}>{this.state.api}</a>
</div>
<div className="modal-footer">
<a className="modal-action modal-close waves-effect waves-green btn-flat">Close</a>
</div>
</div>
...

Let’s break this down. the id attribute is what we are going to use to indicate what modal to open on the link click. You’ll see that in a second. You’ll notice in the href section of the anchor element, I am using a variable referring to this.state.api. We haven’t added that state yet, but we will. We’ll use it to store the API endpoint URL and render it on screen.

Now, if we return to our link button in the header, we just need to give it an href and a className:

<li><a href="#apiModal" className="modal-trigger"><i className="small-menu material-icons">link</i></a></li>

The href points to the id attribute we created in the modal code. The className is Materialize’s shorthand for activating the modal through jQuery. Now, when we save and click that link in the header, the modal should display.

We’re almost there, but you haven’t even “coded” anything, have you? Let’s change that. This is also going to be incredibly simple, but incredibly powerful.

Hacking Blockstack

We’re going to make some minor changes to the JavaScript function provided by Blockstack and used in Graphite’s normal web app. In the normal web app, everything is encrypted. You can see that when you look at the function called autoSave(). That function has some extra code not tied to this tutorial, so focus in on the lines that look like this:

else {
putFile(fullFile, JSON.stringify(this.state.singleSheet), {encrypt: true})
.then(() => {
console.log("Autosaved");
this.saveCollection();
})
.catch(e => {
console.log("e");
console.log(e);
});
}

The method provided by Blockstack is called putFile and it takes some parameters:

  • The file name (including extension)
  • The data to be saved in that file, stringified (in this case, this.state.singleSheet contains all the data we need)
  • An object with the encryption set to true or false

Again, Graphite encrypts everything, but since we are making a public API, let’s go ahead and change that true to false.

Finally, we need to create a state of api and set that state here. So, in the constructor at the top of the file, you’ll see all the states currently being tracked. At the bottom of the list add api so it looks like this:

...
revealModule: "innerStealthy",
decryption: true,
api: ""
}
...

Then in the autoSave() function, we want to set that state after every save. Since Graphite makes use of Promises, we can just chain on a .then right after the file is saved and right before the next function is called. That’s where we can set state. The entire putFile function we’re using will look like this now:

...
else {
putFile(fullFile, JSON.stringify(this.state.singleSheet), {encrypt: false})
.then(() => {
this.setState({ api: SOMETHINGHERE })
})
.then(() => {
console.log("Autosaved");
this.saveCollection();
})
.catch(e => {
console.log("e");
console.log(e);
});
}

But we need to figure out where to get our API endpoint URL. This take a little bit of understanding of Blockstack’s inner workings. Or, we can take a shortcut! Since this is a tutorial, we’re going to take a shortcut.

There’s a lovely gateway tool provided by Blockstack called the Gaia Gateway. You can see it here:

This tool allows you to quickly look up a file if you know where you’re looking. The link above explains how to structure the URL you will use:

/{{bnsName}}/{{applicationHost}}/{{filename}}

{{bnsName}} is your Blockstack username. For example, mine is jehunter5811.id. The {{applicationHost}} is your app’s domain minus the http://www. part. Since we areusing localhost for this tutorial, we will need to URL encode the applicationHost (more on that in a second). Finally, {{filename}} is the name of the file we provided in our code.

Let’s start with the easy way to programmatically grab you {{bnsName}} so you don’t have to hardcode it each time. Blockstack provides a method to handle this. This method is already imported in the code you’re working with, but if you want to see it, it’s at the top of the file in the methods imported from ‘blockstack’ and it’s called loadUserData.

Next, we need to be able to use localhost:3000 for testing in this tutorial, so let’s URL encode that (depending on your app’s domain, if you were to deploy this, you may need to URL encode the origin anyway). There are a number ways to handle this, but I like to use urlencoder.org. We still don’t need th http:// portion of the URL, but we do have to URL encode the rest. Here’s what the encoded URL looks like:

localhost%3A3000

Now, we need to figure out what the file name is. Let’s take a look at the code. If you remember, the function autoSave() is where the file is saved and it’s where you’ll see the file name.

const file = this.props.match.params.id;
const fullFile = '/sheets/' + file + '.json';
putFile(fullFile, JSON.stringify(this.state.singleSheet), {encrypt: false})
.then(() => {
this.setState({ api: SOMETHINGHERE })
})
.then(() => {
console.log("Autosaved");
this.saveCollection();
})
.catch(e => {
console.log("e");
console.log(e);
});

There are a few things to understand here. First, this.props.match.params.id is looking at the “id” portion of the URL as we defined it in our route within BrowserRouter. That looked like this:

<Route exact path="/sheets/sheet/:id" component={SingleSheet} />

So, look at your URL bar and you can see the “id” portion is a bunch of numbers (a timestamp, if you’re interested) that looks like 1539027499068. So, this.props.match.params.id in my case equals 1539027499068. Next, we’ve got the line in autoSave() that says:

const fullFile = '/sheets/' + file + '.json';

This is just a concatenation of a directory path, the document id as we defined it above, and the file extension. Let’s change this up, since this is our API endpoint. I didn’t change the existing structure, so here’s mine:

const fullFile = '/sheets/' + file + '.json';

So, what does this all look like in terms of using it with Gaia Gateway? If you put it all together (https://gaia-gateway.org/{{bnsName}}/{{applicationHost}}/{{filename}}) it would look something like this:

https://gaia-gateway.com/jehunter5811.id/localhost%3A3000//sheets/1539027499068.json

You can actually visit that URL and see my basic example. But again, we don’t want to hard code this into our file, so let’s programmatically define the URL to use in our api link state.

autoSave() {
const file = this.props.match.params.id;
const fullFile = 'api/' + file + '.json';
putFile(fullFile, JSON.stringify(this.state.singleSheet), {encrypt: false})
.then(() => {
this.setState({ api: 'https://gaia-gateway.com/' + loadUserData().username + '/localhost%3A3000/' + fullFile })
})
.then(() => {
console.log("Autosaved");
this.saveCollection();
})
.catch(e => {
console.log("e");
console.log(e);
});
}

Go ahead and save that update, then we’ll need to trigger a save in your spreadsheet file. To do so, enter something in one of the cells. It will auto-save. Once the save is done, click the link in your header to display the API link modal. It should look like this:

Click the link and you should be presented with the JSON print out from your public API! Depending on what you entered and in what cell you entered it, your values will appear differently than mine.

You did it. You made a public API using Graphite as a base!

One final thing you’ll need to do is make sure your spreadsheet can be loaded now that you’ve saved it. As mentioned previously, Graphite encrypts everything. So that means everything is also decrypted when loaded. However, we just changed our spreadsheets to be public and not encrypted. So, when the file is loaded we need to tell the system not to try to decrypt. This is simple enough. Find the function called loadSingle() and find the getFile function within that. Just like we did when saving the file, we need to change the encryption object. In this case, we will be saying {decrypt: false}. Here’s what the start of your getFile function should look like now:

getFile(fullFile, {decrypt: false})

While we’re here, we should probably tell the file to also load up our API link every time the file loads. Otherwise, you’ll open your spreadsheet and find the link blank after initial load. This is also going to be really simple. In that same getFile function within the parent loadSingle() function, we just need to set state for “api”. We’ll set that the same way we did when we created the api link above. Here’s what your full getFile function should look like now:

getFile(fullFile, {decrypt: false})
.then((fileContents) => {
if(fileContents) {
this.setState({ title: JSON.parse(fileContents || '{}').title, grid: JSON.parse(fileContents || '{}').content, api: 'https://gaia-gateway.com/' + loadUserData().username + '/localhost%3A3000/' + fullFile })
}
})
.then(() => {
this.setState({ initialLoad: "hide" });
})
.catch(error => {
console.log(error);
});

Wrapping up

There’s still a lot you can do here, and not everything is perfect. This was simply a tutorial to help you quickly go from nothing to a decentralized public API.

The Graphite spreadsheet component is configurable. It uses Handsontable to help power the spreadsheet elements, and you can set the number of blank cells available when you first load it up. In the tutorial, it’s set to 100. So, when you visit your API link, you’re going to see a bunch of arrays with null in them in addition to the arrays that contain your values. You can adjust this as you see fit.

Handsontable also supports objects instead of arrays. Graphite uses an array of arrays, but you’re welcome to experiment with objects.

There is a significant amount of unused code in your local repository now. You can leave it or you can clean it up. I’ve left the code as is (since it might soon make it into the production Graphite app in some form!), and I’ve pushed the updated SingleSheet.js file to Github at the link below. I have also deployed an example app that utilizes the public API that I created locally here in this tutorial. That link is below as well.

Updated Graphite Sheets code from this article: https://github.com/Graphite-Docs/Hacking-Graphite/tree/master/tutorial-code/api

A fake store built using the public API created in this tutorial: https://confident-borg-b52b6e.netlify.com/

Source code for the fake store: https://github.com/Graphite-Docs/Hacking-Graphite/tree/master/tutorial-code/graphite-store

Remember, you can use the public Graphite document linked to in the beginning of this tutorial to provide feedback, or you can comment on this post. Happy hacking!

--

--

Justin Hunter
The Lead

Writer. Lead Product Manager, ClickUp. Tinkerer.