Summary

This tutorial will help you get started building with the Ceramic protocol within minutes. It covers everything from installing the software, to creating, reading, and updating your first documents. It uses the Ceramic CLI and JavaScript APIs.

Background

Ceramic is a protocol for creating smart documents, which are dynamic, mutable documents controlled by DIDs. Smart documents have a wide variety of use cases which are beyond the scope of this tutorial. Instead, this article provides you with the basics of how to build with Ceramic, specifically on the new Clay devnet. Clay is the final iteration of the protocol before it launches to mainnet later this year. If you’re building on Clay using this tutorial, we would love to hear about your experience in our Discord.

An overview of the Ceramic protocol can be found here, and the software architecture here.

Using the CLI

Installation and Setup

To get started, install the Ceramic CLI tool from npm.

$ npm i -g @ceramicnetwork/cli

Once installed, you can launch the Ceramic daemon. This command spins up a node which runs the Ceramic protocol and exposes an http-api on port 7007 for interacting with the node. In the background an IPFS node is started which is used by Ceramic to route data and messages around the network.

$ ceramic daemon
Swarm listening on /ip4/127.0.0.1/tcp/4002/p2p/QmXoQz228wVdWxCgxZnKUWLFZX34yg4crpKRipXba8A2dB
Swarm listening on /ip4/192.168.188.22/tcp/4002/p2p/QmXoQz228wVdWxCgxZnKUWLFZX34yg4crpKRipXba8A2dB
Swarm listening on /ip4/10.66.178.21/tcp/4002/p2p/QmXoQz228wVdWxCgxZnKUWLFZX34yg4crpKRipXba8A2dB
Swarm listening on /ip4/127.0.0.1/tcp/4003/ws/p2p/QmXoQz228wVdWxCgxZnKUWLFZX34yg4crpKRipXba8A2dB
Ceramic API running on port 7007

Creating your first document

We can now start creating documents! There are different types of documents supported by Ceramic and these are called doctypes. The most common and general-purpose doctype is the tile doctype which will be the only one we concern ourselves with in this tutorial. To create your first document, simply use the ceramic create command as shown below.

$ ceramic create tile --content '{ "title": "My first Document" }'
DocID(kjzl6cwe1jw14aiqfq6zngy43yb5r6oph7zj7zscehevpsyujrjoxk61r4ubwgi)
{
  "title": "My first Document"
}

When the command is executed, a new document is created which has the content that we specified. The first thing that gets printed is its unique document id (DocID).

kjzl6cwe1jw14aiqfq6zngy43yb5r6oph7zj7zscehevpsyujrjoxk61r4ubwgi

The other output is simply the content of the newly created document.

Reading a document

Now we can print the content of the document again by typing ceramic show <DocID>. However more interestingly we can show the state of the document instead by using the ceramic state command. This command shows us more metadata about the document, such as who the owner is, what the status of the anchor is (below it's pending because it was requested but not yet finalized), and what doctype the document has.

$ ceramic state kjzl6cwe1jw14aiqfq6zngy43yb5r6oph7zj7zscehevpsyujrjoxk61r4ubwgi      11:21
{
  "doctype": "tile",
  "content": {
    "title": "My first Document"
  },
  "metadata": {
    "schema": null,
    "controllers": [
      "did:key:z6MkfZ6S4NVVTEuts8o5xFzRMR8eC6Y1bngoBQNnXiCvhH8H"
    ]
  },
  "signature": 2,
  "anchorStatus": "PENDING",
  "log": [
    {
      "cid": "bagcqcera2j452x4qibnplotwnbcagbfkpqgcb3bmjppggt56iqw3feo624ja",
      "type": 0
    }
  ],
  "anchorScheduledFor": "12/23/2020, 10:30:00 AM"
}

Updating a document

So we created a document which has a title but now we also want to have a description in there to give anyone observing the document a bit more context. We can update the document using the ceramic change command.

$ ceramic change --content '{ "title": "My first Document", "description": "This is an example of how a doc can be changed" }' kjzl6cwe1jw14aiqfq6zngy43yb5r6oph7zj7zscehevpsyujrjoxk61r4ubwgi

Creating a schema

Now what if we are creating many documents and want to ensure that they all have both a title and a description. Is there a way to enforce this? Yes of course! We can do so by creating a separate document which contains a json-schema. The schema document describes a specific json structure, and it can be used to validate the structure of the document containing our content. Let’s start by just simply creating a tile that contains the json-schema that we desire.

$ ceramic create tile --content '{                                                   12:04
    "$schema": "http://json-schema.org/draft-07/schema#",
    "properties": {
      "title": {
        "type": "string"
      },
      "description": {
        "type": "string"
      }
    },
    "type": "object",
    "additionalProperties": false,
    "required": ["title", "description"]
  }'

DocID(kjzl6cwe1jw147xappgc4mjr3qn1oe0kir2lsdp4gve8s9245hdpx0qgcc2ftt4)
{
  "type": "object",
  "$schema": "http://json-schema.org/draft-07/schema#",
  "required": [
    "title",
    "description"
  ],
  "properties": {
    "title": {
      "type": "string"
    },
    "description": {
      "type": "string"
    }
  },
  "additionalProperties": false
}

Creating a document that uses a schema

Now that we created the schema document, let’s try to apply it when we create a new document by using the --schema parameter. This parameter only accepts DocIDs which include the commit. This is because we don't want the document schema enforcement to break if the schema changes, so we refer to a specific commit.  In order to list the commits of the document we just created simply run:

ceramic commits kjzl6cwe1jw147xappgc4mjr3qn1oe0kir2lsdp4gve8s9245hdpx0qgcc2ftt4
[
  "k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g"
]

We can now create a document using this schema commit.

$ ceramic create tile --schema k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g --content '{ "title": "A doc with schema" }'
Validation Error: data should have required property 'description'

Whoops, in the example above we forgot to add the description property to the document. This means that our newly created document would not conform to the specified schema and the creation thus results in an error. Let’s try again by adding a description this time.

$ ceramic create tile --schema k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g --content '{ "title": "A doc with schema", "description": "This document has to follow the given schema" }'
ceramic://bafyreiddvykwgrxvmznnast3ky62kejx4oo6nfltmtl5udeatkhapwvaaq
{
  "title": "A doc with schema",
  "description": "This document has to follow the given schema"
}

Cool! We managed to create a document which conforms to a schema defined within another document. Any further changes to the document will also be validated against that schema. That’s pretty neat.

Using the HTTP client

Installation and Setup

If you want to use Ceramic from within a browser or Node.js you can use either the @ceramicnetwork/core or the @ceramicnetwork/http-client package. The first is an implementation of the full protocol, the latter is an http client which connects to a remote Ceramic daemon, which is more lightweight. Both expose the same API, so you can use them interchangeably. In this tutorial we are going to use the HTTP client and connect it to our local daemon. First, just install the package.

$ npm i --save @ceramicnetwork/http-client

In the examples below we use node for simplicity, however all of the example code snippets works just as well in a browser environment.

$ node --experimental-repl-await

First we import and create an instance of the Ceramic HTTP client. By default it will connect to localhost:7007. If you want to connect to a remote instance you can simply pass the url to the constructor like so: new CeramicClient('<https://my-domain.xyz:7007>').

> const CeramicClient = require('@ceramicnetwork/http-client').default

> const ceramic = new CeramicClient()

Reading a document

Now let’s try loading the document that we created in the CLI before.

> const doc1 = await ceramic.loadDocument('kjzl6cwe1jw146huhfhvxio2had29ptcld1l4im1h8s3hjzwdk16gac7vrezz11')
> doc1.content
{
  title: 'A doc with schema',
  description: 'This document has to follow the given schema'
}

Above we simply print the document content which is available in the doc1.content getter. When instantiated like this, the Ceramic client is a read-only client. In order to actually write new documents and update them we need to authenticate using a DID Provider. In the examples below we are using key-did-provider-ed25519.

Updating a document

First install the Ed25519Provider for did-key.

$ npm i --save key-did-provider-ed25519

Then, simply create an instance of the Ed25519Provider and use it’s DID provider with the ceramic client. Note that we are passing a seed which is used to generate the DID. If we use the same seed again we will always get the same DID.

> const { Ed25519Provider } = require('key-did-provider-ed25519')
> const seed = new Uint8Array([
    6, 190, 125, 152,  83,   9, 111, 202,
    6, 214, 218, 146, 104, 168, 166, 110,
  202, 171,  42, 114,  73, 204, 214,  60,
  112, 254, 173, 151, 170, 254, 250,   2
])
> const provider = new Ed25519Provider(seed)
> await ceramic.setDIDProvider(provider)

Alright, so let’s try to change a document!

> await doc1.change({
    content: {
      title: 'A doc with schema',
      description: 'This should not be possible'
    }
  })
Signature was made with wrong DID. Expected: did:3:bafyreibyqmzpkuavkte2vxwu6vpfnc4c62pdac5ycx2y56w342ahvpikle, got: did:3:bafyreiel4qbm5feio2bt5wp4pun2idwbh6oce22njp7l2utzbxyxpakzw4

What, an error? Yes, we tried to update doc1 using our DID from IdentityWallet, but this document was previously created using a different DID on the CLI. Since doc1 is controlled by a different DID from the one we just created above, we won’t be able to change it from here. Let’s instead create a new document. We set the controllers property to our DID which we get by calling ceramic.did.id.

> const doc2 = await ceramic.createDocument('tile', {
    metadata: {
      schema: 'k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g',
      controllers: [ceramic.did.id]
    },
    content: { "title": "Client Document" }
  })
Uncaught Error: Validation Error: data should have required property 'description'

As you can see, we tried to create a document that uses the schema that we defined using the CLI, however in the above example we failed to provide the description property. Now, let’s actually create doc2.

> const doc2 = await ceramic.createDocument('tile', {
    metadata: {
      schema: 'k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g',
      controllers: [ceramic.did.id]
    },
    content: {
      title: "Client Document",
      description: "A Ceramic Document created from the http client."
    }
  })
> doc2.id
DocID(kjzl6cwe1jw147gx6c2z3pdy1hq2h17k7pvkgmhs350gaho9xr9tcuehxgo2t43)
> doc2.content
{
  title: 'Client Document',
  description: 'A Ceramic Document created from the http client.'
}

We have now created a new document owned by the DID from our Ed25519Provider instance. The schema of this document is however controlled by the DID from the CLI. This illustrates how we can make complex graphs of interconnected documents where different documents are controlled by different identities.

As in the CLI we can also change a document from the javascript API.

> await doc2.change({
    content: {
      title: 'Client Document',
      description: 'This document is now also updated from the http client'
    }
  })
> doc2.content
{ title: 'Client Document', description: 'This document is now also updated from the http client' }

That’s it for this tutorial! Hope you found it useful and thanks for reading. If you want to dive deeper have a look at the js-ceramic Github repo, and jump into our Discord.