Exploring the Solid Ecosystem and Writing Solid Apps

By rhyzom | rhyzom | 4 Apr 2020


So, we've talked about Solid already I think. It's Tim Berners-Lee's latest project, an undertaking towards re-decentralizing the web and ensuring privacy. Somewhat similar to Urbit, it's a personal data/cloud server called a pod (you can get one here) — with access control permissions and transparency in how things work. It's one of those projects that are loosely in the same category as Holochain, Urbit, Scutterbutt, etc.

351665157-c809824b19d74a6465191b30ca00d52a92b2d7846f6e8e890a12c65ef099b279.png 

Upon signing up for a pod, remember your Solid Pod URL and WebID, which you will use to log in the various Solid apps and authorize their access control privileges.

But what makes Solid special is its extensive use of Linked Data (also covered more extensively in a previous recent post on web ontologies). What Solid implements as a built-in capability with semantic technologies like Linked Data and JSON-LD is the faculty to translate raw data into meaningful information. And the way it makes sense of it is by organizing and cataloging it into subject-predicate-object statements about the world (called triples in RDF). And the scope/reach of a globally distributed web of actor-networks with near-instant speed of communication in this case would enable unprecedented acquisition of knowledge beyond the limits of our human horizons.

Linked data is represented in RDF using the Turtle syntax to formulate triples. Basically, writing Turtle comes down to writing down the three components of a Linked Data link: the subject (source of the link), the predicate (the link type), and the object (target of the link). Those three together are called a triple. Here's an example of a triple:

<https://janespod.solid/profile/card#me> <http://xmlns.com/foaf/0.1/name> "Jane Doe"@en.

 

We bracket URLs with <> and literal values with quotation marks "", the @en tag indicates the literal use of English language and the dot ends the triple. The above triple translates as "Jane's full name is Jane Doe." 

Manipulating Linked Data with rdflib.js

 

The easiest way to work with linked data in Solid is by using a library called rdflib.js — a toolbox for doing most things related to linked data (e.g., store, parse, serialize into different formats, keep track of changes coming from an app or a server, etc.)

To install rdflib, use the npm package manager:

npm install rdflib --save

Afterwards you'll need to include the following line in your code:

const $rdf = require(‘rdflib’)

A store is a data structure for storing graph data (linked data) and that queries can be performed against. To set up a store:

const store = $rdf.graph();

And these are the types of nodes in the RDF graph (called terms as they are like terms in a language, assuming RDF to be a language).

What How Term type Node identified by a URI x = $rdf.sym(uri) symbol Blank node x = $rdf.bnode() 'bnode' Untyped Literal x = $rdf.literal('abc') literal Typed Literal x = $rdf.literal('8080', undefined, XSD('int')) literal Literal with language x = $rdf.literal('car', 'en') literal Ordered list x = $rdf.list([node1, node2]) collection

 

There are two ways to look at RDF data in a store — either synchronously use the each() and any() methods and statementsMatching(), or do a query (returning results asynchronously). The each()any() and statementsMatching() take a pattern of subject, predate, object and source, where for each() and any() one of the components in the triple are undefined and source may be undefined or not. For example, using $rdf.sym() to make an object for an RDF node (symbol),

var me = $rdf.sym('https://www.w3.org/People/John-Doe/card#i'); var knows = FOAF('knows') var friend = store.any(me, knows) // Any one person

The add(s, p, o, w) method allows a statement to be added to a formula. The optional w argument can be used to keep track of which resource was the source (URI) for each triple.

A full tutorial of rdflib.js is found here.

Writing a Solid App

Writing a Solid app involves the authentication of the user's WebID, the setting up a data model and the reading and writing of data on a user's pod. There's sufficient tooling and Solid-specific libraries available which make the process fairly easy. And, of course, it all involves a lot of handling of linked data and RDF triples. Solid apps are usually written in either Angular or React (Javascript) web frameworks.

The easiest way to get started is to use the solid-angular yeoman generator. Yeoman is a scaffolding tool that will install all the basic files, folders, and dependencies that you will need to start coding right away. The solid-angular generator for that purpose can be found here.

In a command line window, follow these steps:

  1. $ npm install -g install @inrupt/generator-solid-angular 
  2. Go to the root project folder the app is to live in
  3. $ yo @inrupt/solid-angular
  4. Set an application name / folder name
  5. Angular files and dependencies are installed with a sample application ready to go

Once these steps are complete, you will have a sample application showing the basics of a Solid app. It will be able to login users in via Solid, and authenticate, fetch data from a pod, and update or delete data from a pod. You can start the application using angular-cli as usual, simply by using ng serve.

1. Authentication

The starting point for any Solid Web App is to obtain the user’s WebID. The WebID is a URL at which one can find information about the user, and where to read or write data. The WebID will be provided by the user’s Identity Provider (which is usually their Pod Provider), but any interaction with a user’s Pod will require explicit permission to be granted by the Pod owner.

To obtain this permission, we will be using solid-auth-client. Its usage is relatively straightforward:

  1. Check if the user has already logged in.
  2. If not, ask the user for their Identity Provider.
  3. Then call auth.login() with the Identity Provider as the first argument.

You can ask the user for their Identity Provider using a regular <input type="url">, or using a convenience library that suggests the most commonly used Providers — at the time of writing, solid.community and inrupt.net.

In code, that process would look roughly as follows:

import auth from 'solid-auth-client'; 

async function getWebId() { 
	/* 1. Check if we've already got the user's WebID and access to their Pod: */ 
	let session = await auth.currentSession(); 
	if (session) { 
	  return session.webId; 
	} 
	/* 2. User has not logged in; ask for their Identity Provider: */ 
	// Implement `getIdentityProvider` to get a string with the user's Identity Provider (e.g. 
	 `https:inrupt.net` or `https://solid.community`) using a method of your choice. 
	const identityProvider = await getIdentityProvider(); 
   /* 3. Initiate the login process - this will redirect the user to their Identity Provider: */ 
   auth.login(identityProvider); 
}

2. Understanding Solid 

Now that we’ve got the user’s WebID, we have a starting point for fetching data from the user’s Pod.

What’s needed here is unique terms that have an agreed-upon definition. And just like we can have a Document describing me, we could also make Documents describing a term. And in fact, many people have done exactly that, for many different terms you might want to use. These Documents are called Vocabularies, and there’s one for things you might want to put on a business card at http://www.w3.org/2006/vcard/ns — the vCard Vocabulary. It contains Statements along the lines of:

Subject Predicate Object #role label Role #role comment To specify the function or part played in a particular situation #organization-name label Organization name #organization-name comment To specify the organizational name

(As a shorthand for http://www.w3.org/2006/vcard/ns#role, we will use vcard:role.)

So now we can use vcard:role, and be relatively confident that every other app using it will use it in the way described at that URL. We can combine terms from different Vocabularies, e.g. the FOAF (“Friend of a friend”) vocabulary has a term to refer to a person’s name at http://xmlns.com/foaf/0.1/name. My Document could thus look something like this:

Subject Predicate Object #me foaf:name Vincent #me vcard:organization-name inrupt #me vcard:role Developer

Everything that needs to be uniquely defined has a URL, with some Literal values for the rest (“Vincent”, “inrupt”, and “Developer”). You could imagine “inrupt” to be replaced by a URL as well, pointing to a Document describing the organisation itself.

3. Reading Data

Having the user's WebID and knowing it pointing to a Document, let's proceed reading data from that Document. We'll use Tripledoc for the purpose (although there are other libraries too,such as ldflexrdf-ext and rdflib), which is intentionally limited in scope and designed to aid "thinking in Solid". 

We’ll attempt getting the user’s name like that:

  1. Fetching the Document that lives at their WebID.
  2. From that Document, reading the Subject representing the user’s profile.
  3. Getting the foaf:name of that Subject, if such is set.

In code, the above looks like this:

import { fetchDocument } from 'tripledoc'; 

async function getName(webId) { 
	/* 1. Fetch the Document at `webId`: */ 
	const webIdDoc = await fetchDocument(webId); 
	/* 2. Read the Subject representing the current user's profile: */ 
	const profile = webIdDoc.getSubject(webId); 
	/* 3. Get their foaf:name: */ 
	return profile.getString('http://xmlns.com/foaf/0.1/name') 
}

To avoid typing the full ‘http://xmlns.com/foaf/0.1/name’ every time one can use the library rdf-namespaces. It exports strings for the URLs of the terms in common Vocabularies, turning the above into:

import { fetchDocument } from 'tripledoc'; 

async function getName(webId) { 
	/* 1. Fetch the Document at `webId`: */ 
	const webIdDoc = await fetchDocument(webId); 
	/* 2. Read the Subject representing the current user's profile: */ 
	const profile = webIdDoc.getSubject(webId); 
	/* 3. Get their foaf:name: */ 
	return profile.getString(foaf.name)
}

Two things to note here. First, we call getString to indicate that we are looking for an actual value (i.e. a Literal), rather than a URL. (Likewise, we could use getInteger or getDecimal if we expected a number instead.) However, the value could also have been a URL pointing to a different Subject, in which case we could in turn fetch that Document. If that was what we expected, we could have used the method getRef instead.

The second thing to consider is that we cannot make any assumptions about what data is, or is not, present in the user’s Pod. Thus, profile.getString(foaf.name) might also return null. This could happen if the Document does not include the user’s name, if the name is stored differently (e.g. using foaf:firstName and foaf:familyName), or the value of foaf:name is not a literal string.

Now that we’re able to read data from the user’s WebID, let’s find out how we can read arbitrary other data.

4. Setting up a Data Model

Now it’s time to start working on some actual functionality: we’ll be making an app that allows people to keep notes. In this step, we’ll prepare their Pod for our data model — much as you might prepare database tables in a traditional application with a relational back-end.

At a high level, we’ll set up the data model as follows:

  1. Check if a Document tracking our notes already exists.
  2. If it doesn’t exist, create it.
  3. Fetch that Document.

So to start with the first step: in which Document should we track notes? The answer can be found in the concept of the Public Type Index.

The Public Type Index is itself a publicly accessible Document stored in the user’s Pod. This Document contains a list of links to other Documents, along with the type of data that is to be included in those Documents. To store notes, the data type we will use is the TextDigitalDocument, defined by Schema.org. Every time the user saves a note, we will store it as a TextDigitalDocument.

If the Document containing these notes was located in the user’s Pod at /public/notes.ttl, their Public Type Index could refer to it like this:

Subject Predicate Object #notes rdf:type solid:TypeRegistration #notes solid:forClass schema:TextDigitalDocument #notes solid:instance /public/notes.ttl

The above Type Index includes one Type Registration, identified by #notes, that registers /public/notes.ttl for the data type schema:TextDigitalDocument.

So how do we find the user’s Public Type Index? It’s usually listed in their profile, i.e. the Document accessible at their WebID:

Subject Predicate Object #me solid:publicTypeIndex /settings/publicTypeIndex.ttl

This is a pattern you’ll often encounter when writing Solid apps: you start with the user’s WebID, read the profile Document located there, and from there you can find the data you need.

So let’s see what this looks like in code:

import { fetchDocument } from 'tripledoc'; 
import { solid, schema } from 'rdf-namespaces'; 

async function getNotesList(profile) { 
	/* 1. Check if a Document tracking our notes already exists. */ 
	const publicTypeIndexRef = profile.getRef(solid.publicTypeIndex); 
	const publicTypeIndex = await fetchDocument(publicTypeIndexRef); 
	const notesListEntry = publicTypeIndex.findSubject(solid.forClass, schema.TextDigitalDocument); 

   /* 2. If it doesn't exist, create it. */ 
   if (notesListEntry === null) { 
	 // We will define this function later: 
	 return initialiseNotesList(profile, publicTypeIndex); 
   } 

   /* 3. If it does exist, fetch that Document. */ 
   const notesListRef = notesListEntry.getRef(solid.instance); 
   return await fetchDocument(notesListRef); 
}

Given the profile we fetched before, we can find a reference to the public Type Index under solid:publicTypeIndex. Then in the public Type Index, we can find the #notes entry by looking for the Subject that has a Statement saying it is for the class schema:TextDigitalDocument.

If that Subject is not found, we initialise a new Document to contain the notes — more on that later. If it is found, we find the URL of the Document that should contain notes under the solid:instance key. All we then have to do is to call fetchDocument to retrieve the Document at that URL.

One thing to keep in mind, again, is that you can make no assumptions about the data in the user’s Pod. Thus, to be safe you should always check whether the return value of getRef() is null. Consider using TypeScript to get a warning when you forget to do so.

That leaves us with one loose thread: the function initialiseNotesList(), which creates a new Document that will contain the notes, and adds it to the Public Type Index. Let’s take a look:

import { createDocument } from 'tripledoc'; 
import { space, rdf, solid, schema } from 'rdf-namespaces'; 

async function initialiseNotesList(profile, typeIndex) { 
   // Get the root URL of the user's Pod: 
   const storage = profile.getRef(space.storage); 

   // Decide at what URL within the user's Pod the new Document should be stored: 
   const notesListRef = storage + 'public/notes.ttl'; 
   // Create the new Document: 
   const notesList = createDocument(notesListRef); 
   await notesList.save(); 

   // Store a reference to that Document in the public Type Index for `schema:TextDigitalDocument`: 
   const typeRegistration = typeIndex.addSubject(); 
   typeRegistration.addRef(rdf.type, solid.TypeRegistration) 
   typeRegistration.addRef(solid.instance, notesList.asRef()) 
   typeRegistration.addRef(solid.forClass, schema.TextDigitalDocument) 
   await typeIndex.save([ typeRegistration ]); 

   // And finally, return our newly created (currently empty) notes Document: 
   return notesList; 
}

(Keep in mind that, in order to write to the user’s Pod, the user will need to have given your app explicit permission to write when they signed in.)

If all the above looks terribly complicated: that’s because it is. Work is underway to make this easier in the future, allowing you to point a library at any data model, and having the library make sure that the user’s Pod is automatically prepared to handle that model. For now though, you’ll have to work through these step-by-step instructions.

5. Writing Data

Now that the user’s Pod is all set up, it’s time to store some actual notes. Luckily, now that we already have our notesList, most of the heavy lifting is already done:

async function addNote(note, notesList) {
  // Initialise the new Subject:
  const newNote = notesList.addSubject();

  // Indicate that the Subject is a schema:TextDigitalDocument:
  newNote.addRef(rdf.type, schema.TextDigitalDocument);

  // Set the Subject's `schema:text` to the actual note contents:
  newNote.addString(schema.text, note);

  // Store the date the note was created (i.e. now):
  newNote.addDateTime(schema.dateCreated, new Date(Date.now()))

  const success = await notesList.save([newNote]);
  return success;
}

With the user’s Pod fully set up, the above is all there is to it.

 

Solid Apps (List)

 

A list of Solid apps can be found here. These include:

 

Example of a Solid App: Plume, a Solid-based Blogging Platform

 

68747470733a2f2f646569752e6769746875622e696f2f736f6c69642d706c756d652f696d672f6c6f676f2e706e67

Plume is a client-side blogging platform built around the Solid standards. Plume makes use of Markdown and doesn't currently support dynamic configuration of data spaces, meaning it must be run from one's own Web server or manually uploaded to one's account (can use solidtest.space as storage). 

Configuration

To use Plume you have to first manually set some config values in config.json (copy/rename the config-example.json file). Set the postsURL value to have it point to an existing container on a Solid-friendly server storing your blog posts. Finally, you should also set the owners variable by adding your own WebID (a minimal identity spec that defines the use of URIs as identity tokens for recognizing users, while also, importantly, being machine-readable syntax), in order to be able to access the editor interface and create new posts.

Here is an example of the configuration file:

{ 
   "owners": ["https://example.org/profile#me"], 
   "title": "Plume", 
   "tagline": "Light as a feather", 
   "picture": "img/logo.svg", 
   "fadeText": true, 
   "showSources": true, 
   "cacheUnit": "days", 
   "defaultPath": "posts", 
   "postsURL": "https://account.databox.me/Public/blog/posts/" 
}

And here's what each config parameter means:

  • owner: a list of URLs (WebIDs) of the people who can post on the blog
  • title: the title of the blog
  • tagline: tagline/subtitle
  • picture: the picture to display on the blog's header
  • fadeText: true/false - shortens the posts length when viewing the full blog
  • showSources: true/false - it will add a button/link that points to the source of the blog post (the actual resource)
  • cacheUnit: minutes/hours/days/ - validity of certain cached data (you shouldn't really need to change it)
  • defaultPath: this value will be suggested to the user if the blog needs to be initialized
  • postsURL: the URL of the folder (container) holding the posts for the blog

Solid-specific Libraries and Tooling

An assorted list of libraries, tools, clients, etc. by category.

Authentication and file access

Manipulating data

Managing access control

Build interfaces

React components

Angular SDK

WebSocket

RDF manipulation and querying libraries

JavaScript

Python

Java

PHP

RDF tools

Identity management libraries

JavaScript

 

How do you rate this article?

5


rhyzom
rhyzom

Verum ipsum factum. Chaotic neutral.


rhyzom
rhyzom

Ad hoc heuristics for approaching complex systems and the "unknown unknowns". Techne & episteme. Verum ipsum factum. In the words of Archimedes: "Give me a lever and a place to rest it... or I shall kill a hostage every hour." Rants, share-worthy pieces and occasional insights and revelations.

Send a $0.01 microtip in crypto to the author, and earn yourself as you read!

20% to author / 80% to me.
We pay the tips from our rewards pool.