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 streams. It uses the Ceramic CLI and JavaScript APIs.

Background

Ceramic is a public, permissionless, open source protocol for all types of data structures stored on the decentralized web. Ceramic has 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 Ceramic can be found here, and more technical details about the protocol 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 streams

We can now start creating streams on Ceramic! There are different types of data structures built on streams that are supported by Ceramic and these are called StreamTypes. The most common and general-purpose StreamType is the TileDocument which will be the only one we concern ourselves with in this tutorial. To create your first stream, simply use the ceramic create command as shown below.

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

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

kjzl6cwe1jw14aiqfq6zngy43yb5r6oph7zj7zscehevpsyujrjoxk61r4ubwgi

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

Reading a stream

Now we can print the content of the stream again by typing ceramic show <StreamID>. However more interestingly we can show the full state of the stream instead by using the ceramic state command. This command shows us more metadata about the stream, 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 type of stream it is.

$ ceramic state kjzl6cwe1jw14aiqfq6zngy43yb5r6oph7zj7zscehevpsyujrjoxk61r4ubwgi      11:21
{
  "type": 0,
  "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 stream

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

$ ceramic update --content '{ "title": "My first Document", "description": "This is an example of how a stream 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 TileDocument 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 TileDocument 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"]
  }'

StreamID(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 StreamIDs 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 stream we just created simply run:

ceramic commits kjzl6cwe1jw147xappgc4mjr3qn1oe0kir2lsdp4gve8s9245hdpx0qgcc2ftt4
[
  "k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g"
]

We can now create a TileDocument using this schema commit. Please note that the StreamID and CommitID seen above are just for this example.  You will want to replace these with the ids you see in your shell when running the above commands.

$ 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 required packages.

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

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 stream

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

> const { TileDocument } = require('@ceramicnetwork/stream-tile')

> const doc1 = await ceramic.loadStream<TileDocument>('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 and the additional supporting packages.

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

Then, create an instance of the Ed25519Provider and use its 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 KeyDidResolver = require('key-did-resolver').default
> const { DID } = require('dids')

> 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)
> const resolver = KeyDidResolver.getResolver()
> ceramic.did = new DID({ provider, resolver })
> await ceramic.did.authenticate()

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

> await doc1.update({
      title: 'A doc with schema',
      description: 'This should not be possible'
  })
Signature was made with wrong DID. Expected: did:key:bafyreibyqmzpkuavkte2vxwu6vpfnc4c62pdac5ycx2y56w342ahvpikle, got: did:key: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 TileDocument.create(
    ceramic,
    { "title": "Client Document" },
    {
      schema: 'k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g',
      controllers: [ceramic.did.id]
    },
  })
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 TileDocument.create(
    ceramic,
    {
      title: "Client Document",
      description: "A Ceramic Document created from the http client."
    },
    {
      schema: 'k3y52l7qbv1fry0cs6t068wcgqlfvxfo1xciixtenrz9agwf1izlkat847ptcjz0g',
      controllers: [ceramic.did.id]
    },
  )
> doc2.id
StreamID(kjzl6cwe1jw147gx6c2z3pdy1hq2h17k7pvkgmhs350gaho9xr9tcuehxgo2t43)
> doc2.content
{
  title: 'Client Document',
  description: 'A Ceramic Document created from the http client.'
}

We have now created a new TileDocument stream 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 streams where different streams are controlled by different identities.

As in the CLI we can also update a stream from the javascript API.

> await doc2.update({
    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.