IDX is a protocol for building applications with user-centric, interoperable data. In a previous post, we described what IDX is and the benefits of building applications in this way. This tutorial will walk through the creation of a simple note-taking web app using decentralized technologies for authentication and data storage, allowing users to have complete ownership over their contents.
In this tutorial, we will use the following technologies:
React: a popular framework for building web applications
IDX: a JavaScript/TypeScript framework for user-centric data management
Ceramic: a decentralized network for storing mutable, verifiable data
DIDs: a W3C standard for decentralized identifiers
Environment setup
To get started using IDX, we'll first need to install the IDX CLI using npm. You'll also need to have node installed.
npm install -g @ceramicstudio/idx-cli
Project setup
Initial dependencies
We'll use Create React App with the TypeScript template to setup our project:
Finally, as the uint8arrays package does not provide TypeScript definitions, let's add them to the src/react-app-env.d.ts file:
Local Ceramic node
Let's start a local Ceramic node using the script defined, and keep it running for all the steps of this tutorial:
npm run ceramic
We'll also need to bootstrap the local node with the IDX documents, using the IDX CLI previously installed:
idx bootstrap
Data model
By using Ceramic and IDX, we can create a data model for our application that is user-centric (instead of application-siloed), globally available from any client on the Ceramic network, and publiclydiscoverable and shareable across any application(s) that a user interacts with. This is all made possible thanks to public schemas, data definitions, and the user's IDX document. Thus, by storing your application's user data in the IDX framework, it becomes truly user-centric and portable across app domains and contexts.
A schema is a JSON-schema document created by a developer which can be used to validate the contents of other documents simply by including the unique identifier (StreamID) of the schema.
A definition is a document created by a developer which provides metadata about the data that they want to store with the user. The StreamID of the definition acts as a unique key within the user's IDX document, and is used to identify a reference (the actual contents) associated to a specified schema.
An IDX document is a document owned by a user which maintains an index of all of their data in a single place. It is a key-value store that stores mappings from definitions to references.
For this notes management app, we'll use two types of documents and therefore two schemas:
Note: this document will store the contents of a single note
NotesList: the entry-point document to index the list of notes with metadata
We will need to create and publish these schemas to the local Ceramic node, along with a definition which allows us to interact with the notes list using IDX. This will allow our app and others to access the notes via the following path:
User DID —> IDX index —> notes definition —> notes list —> note 1
-> note 2
-> ...
Let's create a bootstrap.js script that will create and publish the schemas and definition to our Ceramic node as TileDocuments, and store the Ceramic stream ID (StreamID) of the definition to a JSON file that will be used by the app:
Now to run this script, we'll need to provide a 32 bytes base16-encoded string as an environment variable:
SEED=<your seed> npm run bootstrap
If you need a simple way to create such a seed, you can use the following command:
Running the bootstrap script will create a config.json file in the src folder, that will be imported by our app. The schemas and definition StreamIDs contained in this config.json file are globally unique and can be shared with other apps wanting to interact with the notes associated to the user.
Interacting with our data model
Before moving forwards with implementing our app, let's first check we can interact with our data model using the IDX CLI.
First, we'll need to create a local DID that will be used by the CLI:
idx did:create --label=local
The local alias can be used to reference the DID in the following commands, rather than having to provide the full DID string.
Now we can create a note with some text using the Note schema URL added to src/config.json. In my case this URL is ceramic://kjzl6cwe1jw14atxo8ax0mrknm7xfh8pxqy24hbdrxi9nagtwoa3la5s4hf32qr but it will be different when using another seed, so don't forget to change it in the example below:
idx tile:create local '{"text":"My first note"}' --schema=ceramic://kjzl6cwe1jw14atxo8ax0mrknm7xfh8pxqy24hbdrxi9nagtwoa3la5s4hf32qr
Successfully running this command will display the stream ID of the created note, that we can then add to the list of notes using the notes definition key added to src/config.json:
idx index:set local kjzl6cwe1jw14almqh93rmt1mpv8h6zvyeyhh7jzlqp95kgv38c0jb5yuo8mo7w '{"notes":[{"id":"ceramic://kjzl6cwe1jw147xhyjemd6r38812gzhhexw9mj3gz2vvkimotu5xktssik2cbzp","title":"First"}]}'
Make sure to replace the definition key (kjzl6cwe1jw14almqh93rmt1mpv8h6zvyeyhh7jzlqp95kgv38c0jb5yuo8mo7w in my case) and the created note URL (ceramic://kjzl6cwe1jw147xhyjemd6r38812gzhhexw9mj3gz2vvkimotu5xktssik2cbzp in the code above) with the values created in your environment.
We can now check the created note and notes list can be loaded:
idx index:get local kjzl6cwe1jw14almqh93rmt1mpv8h6zvyeyhh7jzlqp95kgv38c0jb5yuo8mo7w
idx tile:get ceramic://kjzl6cwe1jw147xhyjemd6r38812gzhhexw9mj3gz2vvkimotu5xktssik2cbzp
Application dependencies and IDX setup
Our app is going to use the following additional dependencies:
Then, let's create an idx.ts file in the src folder, implementing the logic to authenticate using a provided seed and load notes with IDX:
Application state and actions
In this section, we'll implement the core logic of our app based on a state, synchronous actions mutating the state, and high-level handlers performing one or more actions.
First, let's create a state.ts file in the src folder with the following initial contents:
Here we are importing types and dependencies from React (the useCallback and useReducer hooks) and defining the valid shapes the application State can have.
Next we'll define an Action type that includes all the possible synchronous actions that can be performed to mutate the State:
To handle the state transitions, we'll use a reducer function, as presented in React's documentation:
Finally, let's create a React hook wrapping this logic in high-level handlers:
Application UI
Now that our application logic is implemented, we can add the user interface, based on Material UI components.
For simplicity in this tutorial all the components are implemented in a single file, but more complex apps would gain from having the interface split into different modules.
Let's change the generated App.tsx file in the src folder, first to import the dependencies we'll use:
Next, let's create the styles we will use:
Now we'll create our first component: NotesList. This component displays the list of notes in a side menu, and a button to open a draft note.
This component will get the State and needed actions injected:
Another component we will need is AuthenticateScreen, that will display the authenticated ID or seed prompt as needed:
Next, let's add the DraftScreen, allowing users to create and save a new note:
We will use another component, NoteScreen, for displaying an existing note as the logic is a bit different from a draft, notably that we have to first load the document from Ceramic before being able to display the note contents:
Finally, we can use all these components in the top-level App component exported by this module:
That's it!
You should now be able to start the app using the npm start command that will compile and start a local server.
The application logic and UI in this tutorial is intentionally kept simple for demonstration purposes, but could be greatly improved for a real app.
The full code for this tutorial is available on GitHub if you want to use it as a basis for your own experiments.
Moving to production
Using a local Ceramic node is an easy way to get started developing an app, but what about production use cases?
We are working with infrastructure partners who are eager to provide production-grade Ceramic node hosting services that we will recommend once ready. Other options are for you to host a Ceramic node for your users, or alternatively, build your app using a full Ceramic node in-browser with @ceramicnetwork/core (instead of the @ceramicnetwork/http-client used in this tutorial). Future tutorials and guides will dive into how to set up production Ceramic deployments.
In the meantime, feel free to reach out on the Ceramic Discord if you need support deploying Ceramic.