Justin Hunter

Founder of Graphite, co-founder of SimpleID

Build a Versioning System With IPFS and Blockstack

There are so many great use cases for versioning. Handling code deployments, document edits, and database snapshots are just a few immediate uses that come to mind. Normally, a versioning system is another segment within a database, but it can be so much more when you think of it through the lens of immutable data and DHT (distributed hash tables) technology. So, today, we’re going to build a stream of consciousness note-taking app with version history. This will be different than other notes apps as it will have just ONE note that the user can edit over time, removing information or adding information. But we’ll include versions so they can grab their history. We’ll do all that by using Blockstack and IPFS.
Blockstack is a decentralized application platform that lets users choose where there data is stored. For the similicity of this tutorial, we’re going to use the storage hub provided by Blockstack the company (it’s free and there’s no configuration needed). IPFS a peer-to-peer network that allows data to be served based on its content, not its location. This means that when the data changes, it is represented by a different identifier (a hash), and the old version of the data still exists, unchanged. This is perfect for a versioning system. We’re going to build all of this by creating a new React project and installing just one dependency: SimpleID.
SimpleID provides developer tools for the decentralized web. In a nutshell, SimpleID lets developers add decentralized authentication and storage to their apps without asking their users to go through the cumbersome process of generating seed phrases and managing those 12-word backups. Users get a traditional username/password authentication flow while still owning their identity and getting access to Web 3.0 technology.
To get started, visit SimpleID and sign up for a free developer account. Once you verify your account, you’ll be able to create a project and select the Web 3.0 modules to include in your project. Let’s walk through that quickly:
1. Sign up for developer account
2. Click the verification link in your email
3. Once your account is verified, you’ll be on the Accounts page where you can create a new project
4. Give that new project a name and a URL where you may eventually host it (this can be a fake url for now as long as it’s https based)
5. Save and then click View Project
6. Copy down your API Key and Developer ID
7. Go to the Modules page and select Blockstack for your Authentication Module and both Blockstack and Pinata for your Storage Module
8. Click Save
That’s it! Now, you’re ready to work. Quick note about Pinata: They provide an IPFS pinning service, so SimpleID uses them behind the scenes to add content to the IPFS network and to pin said content to ensure it is always available. Read more about pinning here.
Let’s build a project. My instructions will be from the MacOS perspective, but those of you on different systems should be able to use similar commands to get started. First, open up your terminal and create the new React project:
npx create-react-app ipfs-blockstack-versioning
When that’s done, change into the directory and then install the SimpleID dependency:
cd ipfs-blockstack-versioning
npm i simpleid-js-sdk
Ok, open the project up in your text editor of choice. We’re not going to spend time with complex folder structure. This is a very basic application designed to show off the power of Blockstack and IPFS. With that in mind, find the src folder and open App.js. At the top of that file add the following right below the import css statement:
import { createUserAccount, login, pinContent, fetchPinnedContent } from 'simpleid-js-sdk';const config = {
  apiKey: ${yourApiKey}, //found in your SimpleID account page
  devId: ${yourDevId}, //found in your SimpleID account page
  authProviders: ['blockstack'], //array of auth providers that matches your modules selected
  storageProviders: ['blockstack', 'pinata'], //array of storage providers that match the modules you selected
  appOrigin: "https://yourapp.com", //This should match the url you provided in your dev account sign up
  scopes: ['publish_data', 'store_write', 'email']  //array of permission you are requesting from the user
}
Ok, now with the SimpleID package imported and this config object (which comes right from the SimpleID Docs), you’re ready to get started. Let’s work on the user interface a bit. As I mentioned, this is going to be a really simple app, so let’s drop in an editor for handling our document. We’ll do this with a script tag in the index.html file rather than installing a dependency via NPM. You can use any WYSIWYG library, but I'm going to use is called Medium Editor. You can find it here.
Your index.html file is located in the public folder. Find it and add this above the title tag:
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">
    <script src="//cdn.jsdelivr.net/npm/medium-editor@latest/dist/js/medium-editor.min.js"></script>
    <title>NoteStream</title>
You’ll note, I set the title of my app here since we were already editing the file. Feel free to use the same name or create your own. Now that we’ve added the stylesheet and the script we need, let’s move to our App.js file which is located in the src folder. We're going to clear everything out of this file and start mostly from scratch. So, update your App.js file to look like this:
import React from 'react';
import './App.css';
import { createUserAccount, login, pinContent, fetchPinnedContent } from 'simpleid-js-sdk';const config = {
  apiKey: ${yourApiKey}, //found in your SimpleID account page
  devId: ${yourDevId}, //found in your SimpleID account page
  authProviders: ['blockstack'], //array of auth providers that matches your modules selected
  storageProviders: ['blockstack', 'pinata'], //array of storage providers that match the modules you selected
  appOrigin: "https://yourapp.com", //This should match the url you provided in your dev account sign up
  scopes: ['publish_data', 'store_write', 'email']  //array of permission you are requesting from the user
}class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userSession,
      content: "", 
      versions: [],
      selectedVersionContent: "", 
      pageRoute: "signup",
      versionPane: false, 
      versionModal: false 
    }
  }
  render() {
    return (
      <div className="App">      </div>
    );
  }
}export default App;
I’ve converted the function component to a class component, but you can do this as a function component with some minor changes to the way state is handled. You can see I have four state variable that I expect to use: userSession (which will be filled from our Blockstack authentication), content (which will be the actual streaming note), versions (which will be our history), selectedVersionContent (which will be used for displaying the actual content of past versions), pageRoute (which is for handling that is displayed on the screen), versionPane (which determines if the version pane is showing), and versionModal (which determines if the version modal is open or not).
I think the first thing we should do is get a signup and sign in screen rendering. Within the <div> with the className of “App”, add some conditional logic with form inputs like this:
render() {
    const { pageRoute, userSession } = this.state;
    return (
      <div className="App">
        {
          pageRoute === "signup" && !userSession.isUserSignedIn() ? 
          <div>
            Sign Up
          </div> : 
          pageRoute === "signin" && !userSession.isUserSignedIn() ?
          <div>
            Sign In
          </div> : 
          <div>
            App Content
          </div>
        }
      </div>
    );
  }
We are obviously going to fill this in with actual content, but this should help illustrate what’s happening. If that pageRoute state is "signup" and the user is NOT logged in, we should show the signup form. If the pageRoute state is "signin" and the user is NOT logged in, we should show the sign in form. Otherwise, we should show the app.
Now, let’s build this out a little. Let’s start by handling the Blockstack userSession state. This is actually pretty simple. At the top of our App.js file, just add this below the import statements:
import { UserSession } from 'blockstack';
import { AppConfig } from 'blockstack'const appConfig = new AppConfig(['store_write', 'publish_data', 'email']);
const userSession = new UserSession({ appConfig });
You should add this to the top of your actions.js file as well below the existing import statement. Blockstack comes installed with SimpleID, so you don't need to add anymore dependencies. Ok, now let's add the necessary sign in and sign up forms to our App.js file:
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      userSession,
      content: "",
      versions: [],
      selectedVersionContent: "",
      pageRoute: "signup",
      versionPane: false,
      versionModal: false,
      username: "",
      password: "",
      email: "",
      loading: false, 
      error: "    
    }
  }  handleUsername = (e) => {
    this.setState({ username: e.target.value });
  }  handlePassword = (e) => {
    this.setState({ password: e.target.value });
  }  handleEmail = (e) => {
    this.setState({ email: e.target.value });
  }  handleSignIn = (e) => {
    e.preventDefault();
  }  handleSignUp = (e) => {
    e.preventDefault();
  }render() {
  const { pageRoute, userSession, username, password, email, error } = this.state;
  return (
    <div className="App">
    {
      pageRoute === "signup" && !userSession.isUserSignedIn() ?
      <div>
        <form onClick={this.handleSignIn} className="auth-form">
          <input placeholder="username" id="username-sign-up" type="text" value={username} onChange={this.handleUsername} />
          <input placeholder="password" id="password-sign-up" type="password" value={password} onChange={this.handlePassword} />
          <input placeholder="email" id="password-sign-up" type="email" value={email} onChange={this.handleEmail} />
          <button type="submit">Sign In</button>
        </form>
        <p>Already have an account? <button onClick={() => this.setState({ pageRoute: "signin" })} className="button-link">Sign In.</button></p>
        <p>{error}</p>
      </div> :
      pageRoute === "signin" && !userSession.isUserSignedIn() ?
      <div>
        <form onSubmit={this.handleSignUp} className="auth-form">
          <input placeholder="username" id="username-sign-in" type="text" value={username} onChange={this.handleUsername} />
          <input placeholder="password" id="password-sign-in" type="password" value={password} onChange={this.handlePassword} />
          <button type="submit">Sign In</button>
        </form>
        <p>Need to sign up? <button onClick={() => this.setState({ pageRoute: "signup" })} className="button-link">Register.</button></p>
        <p>{error}</p>
      </div> :
      <div>
        App Content
      </div>
      }
    </div>
    );
  }
}
export default App;
There’s a lot we’ve added here, but it’s pretty simple to understand. We added the functions to handle the sign up and sign in flow. We added a form to handle each of those inputs as well. We added a state switcher so that someone on the sign in form could switch to the sign up form and vice versa. We also have a paragraph section ready in both the sign up form and the sign in form to handle any error that might happen during sign up or sign in.
With all of this in place, I think we can finally fire up our app and see how well it’s working. From the terminal run npm start.
Hopefully that worked for you. If it did you’ll see a god-awful ugly sign up form. You can switch to the sign in form and switch back as well.We’re not going to be touching much CSS in this tutorial, but we’ve got the start of a functioning app. You may have noticed earlier, I added a state variable called loading. We’re going to use that here in just a second as we actually sign a user up and log them in. We’ll start with the sign up process. And again, for this, we’ll be using the SimpleID Docs.
Find the handleSignUp function and fill it in like so:
handleSignUp = async (e) => {
  e.preventDefault();
  this.setState({ loading: true, error: "" });
  const { username, password, email } = this.state;
  const credObj = {
    id: username,
    password: password,
    hubUrl: 'https://hub.blockstack.org', //This is the default Blockstack storage hub
    email: email
  }  try {
    const account = await createUserAccount(credObj, config);
    localStorage.setItem('blockstack-session', JSON.stringify(account.body.store.sessionData));
    window.location.reload();
  } catch(err) {
    console.log(err);
    this.setState({ loading: false, error: "Trouble signing up..."})
  }
}
We made our function asynchronous because we need to wait for the createUserAccount promise to resolve before we can do anything else. Other that that, we simply followed the docs and added a try/catch. If there’s an error, the error state will be updated and the loading state will be set back to false. The user should see the error message on the screen then. If there’s no error, the localStorage item Blockstack needs is updated and we refresh the window.
One last thing we should do before testing the sign up flow is add a loading indicator. This isn’t going to be anything special, but when signing up, the indicator will replace everything else on the screen. Let’s update our app code JSX to look like this:
<div className="App">
  {
    loading ?
    <div>
    <h1>Loading...</h1>
    </div> :
    <div>    {
      pageRoute === "signup" && !userSession.isUserSignedIn() ?
      <div>
        <div onSubmit={this.handleSignIn} className="auth-form">
          <input placeholder="username" id="username-sign-up" type="text" value={username} onChange={this.handleUsername} />
          <input placeholder="password" id="password-sign-up" type="password" value={password} onChange={this.handlePassword} />
          <input placeholder="email" id="password-sign-up" type="email" value={email} onChange={this.handleEmail} />
          <button type="submit">Sign In</button>
        </form>
        <p>Already have an account? <button onClick={() => this.setState({ pageRoute: "signin" })} className="button-link">Sign In.</button></p>
        <p>{error}</p>
      </div> :
      pageRoute === "signin" && !userSession.isUserSignedIn() ?
      <div>
        <form onSubmit={this.handleSignUp} className="auth-form">
          <input placeholder="username" id="username-sign-in" type="text" value={username} onChange={this.handleUsername} />
          <input placeholder="password" id="password-sign-in" type="password" value={password} onChange={this.handlePassword} />
          <button type="submit">Sign In</button>
        </form>
        <p>Need to sign up? <button onClick={() => this.setState({ pageRoute: "signup" })} className="button-link">Register.</button></p>
        <p>{error}</p>
      </div> :
      <div>
        App Content
      </div>
     }
   </div>
  }
</div>
Let’s test this out now. Go ahead and type a username, password, and email and then click sign up. Assuming that worked, you should have seen the loading screen and then after a few seconds, the user is logged in and the words “App Content” appear. Nice!
But now what? We haven’t handled sign in, and the user can’t sign out. Let’s handle sign out first since it’s really simple. In the section of your app where you have the words “App Content” add a button that calls the handleSignOut function:
<button onClick={this.handleSignOut}>Sign Out</button>
Then make sure to add that function up with your other functions:
handleSignOut = () => {
  localStorage.removeItem('blockstack-session');
  window.location.reload();
}
Give that a try and the user should be signed out. Now, we can work on log in. I hope you remembered your username and password. Let’s wire up the handleSignIn function:
handleSignIn = async (e) => {
  e.preventDefault();
  this.setState({ loading: true, error: "" });
  const { username, password } = this.state;
  const credObj = {
    id: username,
    password,
    hubUrl: 'https://hub.blockstack.org' //This is the default Blockstack storage hub
  }
  const params = {
    credObj,
    appObj: config,
    userPayload: {} //this can be left as an empty object
  }
  try {
    const signIn = await login(params);
    if(signIn.message === "user session created") {
      localStorage.setItem('blockstack-session', JSON.stringify(signIn.body.store.sessionData));
      window.location.reload();
    } else {
      this.setState({ loading: false, error: signIn.message })
    }
  } catch(err) {
    console.log(err);
    this.setState({ error: "Trouble signing in..."})
  }
}
We are using the SimpleID Docs once more to sign in, and most of this code is re-used from the sign up function. We don’t need the email for sign in, and we have to create a params object, but other than that, it’s mostly the same. With that in place, let’s give this a shot.
You should have seen the loading indicator and then your user was signed in. Of course, we just have a sign out button now when a user in logged in. Let’s change that by dropping in our Medium-style editor.
Below your constructor in App.js and above your other functions, let’s add a componentDidMount method:
componentDidMount() {
  var editor = new window.MediumEditor('.editable');
}
This is using window to fetch the MediumEditor script we added to our index.html file. For us to see anything, we need to edit the App Contents section of our JSX. So in the area where you put your sign out button, let’s add something below that to handle the editor:
<div className="editor">
  <h1>NoteStream</h1>
  <p>Start where you left off or shove your thoughts in the middle somewhere. It's up to you!</p>
  <div className="editable"></div>
</div>
Without any css styling this is going to be too ugly to handle. So, let’s just drop a little in to fix that. In the same folder, but in the App.css file, add the following:
.editor {
  max-width: 85%;
  margin: auto;
  margin-top: 100px;
}.editable {
  max-width: 85%;
  margin: auto;
  border: 1px solid #282828;
  border-radius: 3px;
  min-height: 500px;
  padding: 15px;
  text-align: left;
}
We can change this later, but it at least makes the application presentable. You should see something like this:
Not the prettiest thing, but it’ll do for now. We need to be able to handle the changes to the editor, so let’s start there before we even begin to save data. In our componentDidMount lifecycle event, let’s change things a bit:
componentDidMount() {
  var editor = new window.MediumEditor('.editable');
  //We'll load our content here soon
  editor.subscribe('editableInput', (event, editable) => {
    this.setState({ content: editor.getContent(0) });
  });
}
If you remember, we had created a state variable called content to hold the content of our note. We are setting that state on every change in the editor. That means when we are ready to save the note, we can just fetch our data from the content state. Let’s see how that looks by doing two things. We’ll add a save button and we’ll add a saveContent function.
Right where the sign out button is, add a save button below it:
<button onClick={this.handleSignOut}>Sign Out</button>
<button onClick={this.saveContent}>Save</button>
Then, with all your other functions, create the saveContent function:
saveContent = () => {
  const { content, userSession } = this.state;
  console.log(content)
}
We’re going to be using the userSession state in a minute, so I threw it in there. But with this, you should be able to open the developer console, type into the editor, and then hit save. You’ll see the html content.
That means you’re ready to save content and load that content back. Let’s thing this through first, though. We need to save the content to Blockstack’s storage system and IPFS. Blockstack’s storage system will be an overwrite function every time, but for IPFS, we’re going to store a new version to the network. We also need to be able to fetch the IPFS hashes, so we should store that to Blockstack as well. It sounds to me like we have two files to store on Blockstack: content and versions (hashes). But we have to first save to IPFS so that we have the hash result. Let’s start writing that out in our saveContent function.
saveContent = async () => {
  const { content, userSession } = this.state;
  //First we save to IPFS
  const contentToPin = {
    pinnedContent: JSON.stringify(content)
  }const params = {
    devId: config.devId, //your dev ID found in your SimpleID account page
    username: userSession.loadUserData().username, //you logged in user's username
    id: Date.now(), //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 pinnedContent = await pinContent(params);
  console.log(pinnedContent);
}
We’ve added the async keyword to the function and we’ve used the paramaters necessary to post the content to IPFS as given by the SimpleID docs. In some cases, a developer will need to query Pinata for content they previously posted to IPFS. that’s the whole point of the id field. In this case, we’ll be using Blockstack to manage all of our hashes, so we don’t really care what this identifier is except that it’s unique (thus, Date.now()).
Let’s test this out with console open and see how it goes before we move on. Add some content to your editor then hit Save. If all goes well, you should see something like this in the console:
{ message: "content successfully pinned", body: "QmbRshi9gjQ2v5tK4B8czPqm3jEQ3zGzsuQJuQLyti4oNc" }
That body key in the object is an IPFS hash. We want to use that and store it as a version with Blockstack. So let’s tackle that next.
saveContent = async () => {
  const { content, userSession } = this.state;
  //First we save to IPFS
  const contentToPin = {
    pinnedContent: JSON.stringify(content)
  }const params = {
    devId: config.devId, //your dev ID found in your SimpleID account page
    username: userSession.loadUserData().username, //you logged in user's username
    id: Date.now(), //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
  }  if(pinnedContent.message === "content successfully pinned") {
    const newVersion = {
      timestamp: Date.now(),
      hash: pinnedContent.body
    }
    versions.push(newVersion);
    this.setState({ versions });
    const savedVersion = await userSession.putFile("version_history.json", JSON.stringify(versions), {encrypt: true});
    console.log(savedVersion);
  } else {
    console.log("Error saving content");
  }
}
I’ve added a check to make sure the content pinning to IPFS was successful before trying to save the hash to Blockstack. We need to know the time of the version, so we’re building up a newVersion object with the timestamp and the hash itself and then we are pushing that into the versions array. We are then saving this to Blockstack, where something cool is happening.
You can see an object in the putFile call that says encrypt. We are able to encrypt data that easily. Don’t believe me? Here’s the file I used to test this section of the tutorial:
That’s just encryption our version history, which is important, but wouldn’t it be cool to encrypt the content before sending it to IPFS too? Let’s do that before we tackle the last part of saving content. In your saveContent function, right about the contentToPin variable, add this:
const encryptedContent = userSession.encryptContent(JSON.stringify(content), {publicKey: getPublicKeyFromPrivate(userSession.loadUserData().appPrivateKey)});
We need to import the getPrivateKeyFromPublic function as well. So at the top of your App.js file with the other import statements, add:
import { getPublicKeyFromPrivate } from 'blockstack/lib/keys';
And update the contentToPin variable to look like this:
const contentToPin = {
  pinnedContent: JSON.stringify(encryptedContent)
}
We’ll see in a moment if this works. Let’s pick up after setting and saving the version history. So right after the savedVersions line, add this:
const savedVersion = await userSession.putFile("version_history.json", JSON.stringify(versions), {encrypt: true});const savedContent = await userSession.putFile('note.json', JSON.stringify(encryptedContent), {encrypt: false});
console.log(savedContent);
Here’s what I get back in the console log by doing that: https://gaia.blockstack.org/hub/13ygSWdUeU4gPHbqUzEBvmq1LP7TKNnbtx/note.json
Looks like it worked! So, to recap, we are encrypting the content, storing it on IPFS, using the IPFS hash that’s returned to create a new entry in the versions array, saving that to Blockstack, then saving the current version of the note content to Blockstack.
Pretty cool stuff, but we need to be able to fetch content too, right? Initially, there are just two files we need to fetch when the application loads up: The current content (from note.json), and the versions file (from version_history.json). We should do that as soon as the app loads, so this will need to be added to our componentDidMount lifecycle event. Update the entire event like this:
async componentDidMount() {
  const { userSession } = this.state;
  const content = await userSession.getFile('note.json', {decrypt: false});
  const decryptedContent = userSession.decryptContent(JSON.parse(content), {privateKey: userSession.loadUserData().appPrivateKey});
  this.setState({ content: JSON.parse(decryptedContent )});  var editor = new window.MediumEditor('.editable');
  editor.subscribe('editableInput', (event, editable) => {
    this.setState({ content: editor.getContent(0) });
  });  editor.setContent(JSON.parse(decryptedContent), 0);
}
Save that and go back to your app. When it reloads, the content you had saved will now appear in the editor. We’re getting there. We have just a couple more things to do. We need to load the version history, so let’s do that next.
Right below the decryptContent variable, add the following:
const versions = await userSession.getFile('version_history.json', {decrypt: true});this.setState({ content: JSON.parse(decryptedContent), versions: JSON.parse(versions) });
Now, we can start having fun with versions. Let’s make sure we can render our version history first. In the App Contents section of your JSX, below the editor, add the following:
<div className={versionPane ? "versionPaneOpen" : "versionPaneClosed"}>
  <ul>
  {
    versions.map(v => {
     return(
       <li key={v.timestamp}><a href="#" onClick={() => this.handleVersionModal(v.hash)}>{v.timestamp}</a></li>
     )
    })
  }
  </ul>
</div>
We are creating a section to hold the version history. You’ll note the className is conditional on the state variable versionPane. This is because we want to be able to change that variable and open the version history rather than have it open all the time. Let’s add a button up with our sign out and save button called Version History.
<button onClick={() => this.setState({ versionPane: !versionPane })}>Version History</button>
And let’s update our CSS one more time to handle the display of the pane:
.versionPaneOpen {
  position: fixed;
  top: 0;
  right: 0;
  width: 250px;
  z-index: 999;
  border-left: 2px solid #282828;
  height: 100vh;
  background: #eee;
  display: inline;
}.versionPaneOpen {
  display: none;
}
Go ahead and test it out. You should have at least one version saved, so hit that Version History button to toggle the pane open and closed. It’s ugly, but it works.
The last thing we need to do is pop up a modal to show the content of a past version. Let’s get to work on that by adding a function called handleVersionModal.
handleVersionModal = (hash) => {
  const { userSession } = this.state;
  this.setState({ selectedVersionContent: "", versionModal: true });
  fetch(`https://gateway.pinata.cloud/ipfs/${hash}`)
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    const encryptedContent = myJson.pinnedContent;

    const decryptedContent = userSession.decryptContent(JSON.parse(encryptedContent), {privateKey: userSession.loadUserData().appPrivateKey});
    this.setState({ selectedVersionContent: JSON.parse(decryptedContent)});
  });
}
We are using the JavaScript native Fetch API to handle calling out to an IPFS gateway to fetch the content specific to the version we select in the version pane. That content is encrypted and needs to be parsed and decrypted properly to be accessible. But if you console log the decryptedContent variable, you’ll see the content of the version in question is properly being fetched. We are setting that content to the selectedVersionContent state variable and setting the versionModal to true.
Let’s put that all to use to render the past version on screen. Below the version page JSX you wrote earlier, add this:
<div className={versionModal ? "versionModalOpen" : "versionModalClosed"}>
  <span onClick={() => this.setState({versionModal: false})} id="version-close">Close</span>
  {
  selectedVersionContent ?
  <div dangerouslySetInnerHTML={{__html: selectedVersionContent}} />:
  <h3>Loading content for selected version...</h3>
  }
</div>
Now, we need to style that a bit to be manageable. In App.css, add this:
.versionModalOpen {
  display: inline;
  position: fixed;
  text-align: left;
  left: 12.5%;
  top: 15%;
  width: 75%;
  min-height: 500px;
  margin: auto;
  z-index: 999;
  background: #eee;
  padding: 25px;
  border: 1px solid #282828;
  border-radius: 3px;
}.versionModalClosed {
  display: none;
}#version-close {
  position: relative;
  right: 10px;
  top: 10px;
  z-index: 1000;
  cursor: pointer;
}
Let’ give this thing a try now. Open up the version history pane. Click on a past version. A modal should pop up with the content of that version for you to view.
That’s it! We made it. You can now have an endless stream of consciousness note taking system while retaining control of all past iterations via version history. And to top it all off, every version of the note is encrypted with a private key wholly under your control.
Take your new powers and build other cool things and propel Web 3.0 into the mainstream.
If you’d like to see the code for this tutorial, you can find it here.
(Disclaimer: The Author is a co-founder of SimpleID)

Tags

Comments

More by Justin Hunter

Topics of interest