Introduction

nostralink is a Rust crate to write nostr events as linked data (using JSON-LD) and serialize incoming events to an RDF store.

Transforming Nostr events (which are formatted in JSON) to RDF has a few advantages:

  • Meaning: linked data is structured meaningful data. With the rapidly growing Nostr protocol, new event types are regularly added. Maintaining a vocabulary (using JSON-LD contexts) of all the official semantic properties of commonly used Nostr events helps with the interpretation of the information they contain.

  • Querying: By leveraging the power of SparQL, it becomes trivial to write queries to interpret the data contained in events. Consider this simple query, which extracts the user's information contained in metadata (kind 0) events, allowing us to also find the metadata for people we follow (w3nostr:follows) by passing the nip21 URI of our public key as a SparQL variable.

This book will document how nostralink works and how to use it to write linked-data aware nostr applications.

But first, let's have a look at what events look like when they're turned into linked data.

Turtle

Turtle is an RDF serialization format. In this book i'll always show events in the Turtle (often abbreviated as "ttl") format, because it's very easy to read.

Hello Nostr

In pure JSON, a "Hello Nostr" text note looks like this:

{
  "content": "Hello Nostr",
  "created_at": 1743619857,
  "id": "20df611b4b232d890f874555a20ba89eef6ab744e60a62f6be1566a426d65f73",
  "kind": 1,
  "pubkey": "aaaa5db09dc270c7a7368825e6ac5ab55d169c1fea8b2ff9fa6d0c826e5d5d12",
  "sig": "860557ca6532d34bc683c0d1b15006af3d325f401f56f058c7c53864628192d1f9474a78022655be5353a8e76090b7c60f0d5cf0a86666c79e7b8af2ab798db3",
  "tags": []
}

In ttl, it looks like this:

<urn:nostr-event:20df611b4b232d890f874555a20ba89eef6ab744e60a62f6be1566a426d65f73> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/nostr#Event> ;
  <https://w3id.org/nostr#created_at> "1743619857"^^<http://www.w3.org/2001/XMLSchema#integer> ;
  <https://w3id.org/nostr#id> "20df611b4b232d890f874555a20ba89eef6ab744e60a62f6be1566a426d65f73" ;
  <https://w3id.org/nostr#nip21> <nostr:npub14249mvyacfcv0fek3qj7dtz6k4w3d8qla29jl706d5xgymjat5fqac46s2> ;
  <https://w3id.org/nostr#content> "Hello Nostr" ;
  <https://w3id.org/nostr#kind> "1"^^<http://www.w3.org/2001/XMLSchema#integer> ;
  <https://w3id.org/nostr#pubkey> "aaaa5db09dc270c7a7368825e6ac5ab55d169c1fea8b2ff9fa6d0c826e5d5d12" ;
  <https://w3id.org/nostr#sig> "860557ca6532d34bc683c0d1b15006af3d325f401f56f058c7c53864628192d1f9474a78022655be5353a8e76090b7c60f0d5cf0a86666c79e7b8af2ab798db3"

This is a list of triples. A triple is made of a subject, a predicate, and an object. The subject and the predicate are always what we call URIs (Uniform Resource Identifier), while the object can be an URI or an RDF Literal value (integers, floats, strings, URLs, booleans, ...). From these triples, we learn that:

  1. The urn:nostr-event:... URI is a URN identifying our event. Note that it uses the URN Namespace Identifier (NID) nostr-event (nostralink declares and uses a few NIDs, including nostr-event, nostr-tag, etc ...), while the URN's NSS (a value unique within the namespace) is the event's ID.
  2. The event creation's Epoch timestamp is an integer, with a value of 1743619857.
  3. Its ID is 20df611b4b232d890f874555a20ba89eef6ab744e60a62f6be1566a426d65f73.
  4. The NIP21 address of its author, which is an npub URL, is nostr:npub14249mvyacfcv0fek3qj7dtz6k4w3d8qla29jl706d5xgymjat5fqac46s2
  5. Its content is Hello Nostr.
  6. Its kind, an integer, equals to 1 (which is the event kind for text notes).
  7. The public key of its author is aaaa5db09dc270c7a7368825e6ac5ab55d169c1fea8b2ff9fa6d0c826e5d5d12.
  8. It has a signature

Notice that all predicate URIs in the events are relative to https://w3id.org/nostr#. This URI is the URI prefix representing the nostr schema.

Some metadata

Let's see another event kind: metadata. Metadata events actually carry a stringified JSON payload:

{
  "content": "{\"name\":\"nostra\",\"display_name\":\"Nostralink\",\"about\":\"Linked data for your nostr\",\"nip05\":\"nostra@nostrplebs.com\"}",
  "created_at": 1743637259,
  "id": "1860841a997a7e93bca4b782b0c158d4f3b8ced903be14e4047a4a4aacce4d88",
  "kind": 0,
  "pubkey": "1b7262dc2e0213fa6778777eb5588d063ecf8ce243566fb90991b9e6ec6db316",
  "sig": "cf78e5e157d51f83863442dd20c6f6ecf6d66779f85a49fd346ff5a9a90cf5339c8d31aa6b6c06f5fc54ab0ee85f06e98a88cce49c45a73fb8c5aeea2a99ca37",
  "tags": []
}

nostralink makes the content field be interpreted as JSON-LD. Here's what the metadata event looks like as RDF:

<urn:nostr-event:1860841a997a7e93bca4b782b0c158d4f3b8ced903be14e4047a4a4aacce4d88> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/nostr#Event> ;
  <https://w3id.org/nostr#created_at> "1743637259"^^<http://www.w3.org/2001/XMLSchema#integer> ;
  <https://w3id.org/nostr#id> "1860841a997a7e93bca4b782b0c158d4f3b8ced903be14e4047a4a4aacce4d88" ;
  <https://w3id.org/nostr#nip21> <nostr:npub1rdex9hpwqgfl5emcwalt2kydqclvlr8zgdtxlwgfjxu7dmrdkvtqytd45l> ;
  <https://w3id.org/nostr#content> <urn:nostr-content:f960cd453663c1deb68551d6180fe7c6a4a5ff727704e91ef715c48527a445e2> ;
  <https://w3id.org/nostr#kind> "0"^^<http://www.w3.org/2001/XMLSchema#integer> ;
  <https://w3id.org/nostr#pubkey> "1b7262dc2e0213fa6778777eb5588d063ecf8ce243566fb90991b9e6ec6db316" ;
  <https://w3id.org/nostr#sig> "cf78e5e157d51f83863442dd20c6f6ecf6d66779f85a49fd346ff5a9a90cf5339c8d31aa6b6c06f5fc54ab0ee85f06e98a88cce49c45a73fb8c5aeea2a99ca37" .
<urn:nostr-content:f960cd453663c1deb68551d6180fe7c6a4a5ff727704e91ef715c48527a445e2> <https://w3id.org/nostr#name> "nostra" ;
  <https://w3id.org/nostr#display_name> "Nostralink" ;
  <https://w3id.org/nostr#nip05> "nostra@nostrplebs.com" ;
  <https://w3id.org/nostr#about> "Linked data for your nostr" .

Here we see that:

  • The https://w3id.org/nostr#content predicate, for this event, points to a urn:nostr-content:... URI. This URI represents an object that contains the content attributes of the metadata event: name, display_name, etc ..

  • The nostr-content object contains triples for every field of the content object of the nostr event:

<urn:nostr-content:f960cd453663c1deb68551d6180fe7c6a4a5ff727704e91ef715c48527a445e2> <https://w3id.org/nostr#name> "nostra" ;
  <https://w3id.org/nostr#display_name> "Nostralink" ;
  <https://w3id.org/nostr#nip05> "nostra@nostrplebs.com" ;
  <https://w3id.org/nostr#about> "Linked data for your nostr" .

The metadata SparQL query allows you to retrieve metadata events and their properties.

nostra

You can use the command-line tool nostra to create events and show their RDF representation.

Create a note

Usage: nostra note <TEXT>

Arguments:
  <TEXT>
<urn:nostr-event:ee8ed388a5707b6e98473d43417672f5f9ef35fb874c99cda17904540efc7ba6> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/nostr#Event> ;
  <https://w3id.org/nostr#created_at> "1743647753"^^<http://www.w3.org/2001/XMLSchema#integer> ;
  <https://w3id.org/nostr#id> "ee8ed388a5707b6e98473d43417672f5f9ef35fb874c99cda17904540efc7ba6" ;
  <https://w3id.org/nostr#nip21> <nostr:npub1kjmt7sfn8xzxjvegf9m0fu7mcwt6292kqprh38qj7l5g5vut53nq99h50f> ;
  <https://w3id.org/nostr#content> "Hello" ;
  <https://w3id.org/nostr#kind> "1"^^<http://www.w3.org/2001/XMLSchema#integer> ;
  <https://w3id.org/nostr#pubkey> "b4b6bf413339846933284976f4f3dbc397a515560047789c12f7e88a338ba466" ;
  <https://w3id.org/nostr#sig> "e8e178caa6354be58a09ee991dc94b76dddd92fb434b38a17f6e527349219ca5817c85231eb87202240f7582b934ce261fd15f529656fab6f604d36e05833eea" .

Create a metadata event

Usage: nostra metadata -n <NAME> -d <DISPLAY_NAME> -a <ABOUT> -n <NIP05>

Options:
  -n <NAME>              Name
  -d <DISPLAY_NAME>      Display name
  -a <ABOUT>             About
  -n <NIP05>             NIP05
  -h, --help             Print help
<urn:nostr-event:8b35c8b00938d38ae5503e2814a64ab829d292bbc47de1c149afc8e73413107a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/nostr#Event> ;
	<https://w3id.org/nostr#created_at> "1743651766"^^<http://www.w3.org/2001/XMLSchema#integer> ;
	<https://w3id.org/nostr#id> "8b35c8b00938d38ae5503e2814a64ab829d292bbc47de1c149afc8e73413107a" ;
	<https://w3id.org/nostr#nip21> <nostr:npub1kjmt7sfn8xzxjvegf9m0fu7mcwt6292kqprh38qj7l5g5vut53nq99h50f> ;
	<https://w3id.org/nostr#content> <urn:nostr-content:4e606eacec8a15c65ec9985e2f37a42e9fa8c9a7fd288fb09dfc2e8e72f95306> ;
	<https://w3id.org/nostr#kind> "0"^^<http://www.w3.org/2001/XMLSchema#integer> ;
	<https://w3id.org/nostr#pubkey> "b4b6bf413339846933284976f4f3dbc397a515560047789c12f7e88a338ba466" ;
	<https://w3id.org/nostr#sig> "dfeee365fdeefe1cb80f11eb24855df9855d6c2ea0c3fe21cb380e17721b22f03ef12669e69499b3ac6b1f97e14b507297dd9a3b90e9511789a3fb61f5ad1dea" .
<urn:nostr-content:4e606eacec8a15c65ec9985e2f37a42e9fa8c9a7fd288fb09dfc2e8e72f95306> <https://w3id.org/nostr#name> "nostralink" ;
	<https://w3id.org/nostr#display_name> "Nostralink" ;
	<https://w3id.org/nostr#nip05> "nip05@domain.org" ;
	<https://w3id.org/nostr#about> "Linked data for nostr" .

LD note

A JSON-LD note with a headline, language and a body (in CommonMark).

Usage: nostra ld-note [OPTIONS] <TEXT>

Arguments:
  <TEXT>

Options:
  -l <LANG_TAG>      Language
  -h <HEADLINE>      Headline
  -h, --help         Print help

With a en-GB language tag:

{
  "content": "{\"@context\":\"http://nostralink.org/Note\",\"@id\":\"urn:nostr-content:bc9d24643ecd191c73aec42e123fb0477063f7d921031060ca3578128a772f6c\",\"@type\":\"nostralink:Note\",\"body\":\"My note\",\"headline\":\"Watch out!\",\"inLanguage\":{\"@context\":\"http://nostralink.org/Language\",\"@id\":\"urn:nostr-lang:en-GB\",\"@type\":\"nostralink:Language\",\"alternateName\":\"en-GB\",\"bcp47\":\"en-GB\",\"name\":\"English (United Kingdom)\"}}",
  "created_at": 1743944968,
  "id": "5e3987828c1ae2fc3c62e216fa59aab6097408594839934a1e647979b8a4679a",
  "kind": 7101,
  "pubkey": "b4b6bf413339846933284976f4f3dbc397a515560047789c12f7e88a338ba466",
  "sig": "7cb2511785dbb0d80b909f0f0856d4ee507c30f8edd550ab4b4a008df1bc44101d3c01212bd0fe95a1e4ff5d4d95897b87087388146a93a61fd7250fac3f8e37",
  "tags": []
}
<urn:nostr-event:5e3987828c1ae2fc3c62e216fa59aab6097408594839934a1e647979b8a4679a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/nostr#Event> ;
	<https://w3id.org/nostr#created_at> "1743944968"^^<http://www.w3.org/2001/XMLSchema#integer> ;
	<https://w3id.org/nostr#id> "5e3987828c1ae2fc3c62e216fa59aab6097408594839934a1e647979b8a4679a" ;
	<https://w3id.org/nostr#nip21> <nostr:npub1kjmt7sfn8xzxjvegf9m0fu7mcwt6292kqprh38qj7l5g5vut53nq99h50f> ;
	<https://w3id.org/nostr#content> <urn:nostr-content:bc9d24643ecd191c73aec42e123fb0477063f7d921031060ca3578128a772f6c> ;
	<https://w3id.org/nostr#kind> "7101"^^<http://www.w3.org/2001/XMLSchema#integer> ;
	<https://w3id.org/nostr#pubkey> "b4b6bf413339846933284976f4f3dbc397a515560047789c12f7e88a338ba466" ;
	<https://w3id.org/nostr#sig> "7cb2511785dbb0d80b909f0f0856d4ee507c30f8edd550ab4b4a008df1bc44101d3c01212bd0fe95a1e4ff5d4d95897b87087388146a93a61fd7250fac3f8e37" .
<urn:nostr-content:bc9d24643ecd191c73aec42e123fb0477063f7d921031060ca3578128a772f6c> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://nostralink.org/Note> ;
	<http://nostralink.org/headline> "Watch out!" ;
	<http://nostralink.org/inLanguage> <urn:nostr-lang:en-GB> ;
	<http://nostralink.org/commonMarkBody> "My note" .
<urn:nostr-lang:en-GB> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://nostralink.org/Language> ;
	<http://nostralink.org/name> "English (United Kingdom)" ;
	<http://nostralink.org/alternateName> "en-GB" ;
	<http://nostralink.org/bcp47> "en-GB" .

With a fr-FR language tag:

{
  "content": "{\"@context\":\"http://nostralink.org/Note\",\"@id\":\"urn:nostr-content:a936e0a65c1d0d244d02f07b7f720ddf0910e253c219a7755bca3eff955d138a\",\"@type\":\"nostralink:Note\",\"body\":\"Une petite note\",\"headline\":\"Bonjour\",\"inLanguage\":{\"@context\":\"http://nostralink.org/Language\",\"@id\":\"urn:nostr-lang:fr-FR\",\"@type\":\"nostralink:Language\",\"alternateName\":\"fr-FR\",\"bcp47\":\"fr-FR\",\"name\":\"French (France)\"}}",
  "created_at": 1743945094,
  "id": "054503c0a685ce6a48bbb01817889610e96e44b121837ffd1edf57e28feb6e2b",
  "kind": 7101,
  "pubkey": "b4b6bf413339846933284976f4f3dbc397a515560047789c12f7e88a338ba466",
  "sig": "ed2182a3cd1211a82c7481b5abd3689cdd7a43938c88e41dbc742920a25918b9962be48e6f6a4d6b47d655eda08bb73322644de02fdee00939f0c4bc2d8f5627",
  "tags": []
}
<urn:nostr-event:054503c0a685ce6a48bbb01817889610e96e44b121837ffd1edf57e28feb6e2b> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <https://w3id.org/nostr#Event> ;
	<https://w3id.org/nostr#created_at> "1743945094"^^<http://www.w3.org/2001/XMLSchema#integer> ;
	<https://w3id.org/nostr#id> "054503c0a685ce6a48bbb01817889610e96e44b121837ffd1edf57e28feb6e2b" ;
	<https://w3id.org/nostr#nip21> <nostr:npub1kjmt7sfn8xzxjvegf9m0fu7mcwt6292kqprh38qj7l5g5vut53nq99h50f> ;
	<https://w3id.org/nostr#content> <urn:nostr-content:a936e0a65c1d0d244d02f07b7f720ddf0910e253c219a7755bca3eff955d138a> ;
	<https://w3id.org/nostr#kind> "7101"^^<http://www.w3.org/2001/XMLSchema#integer> ;
	<https://w3id.org/nostr#pubkey> "b4b6bf413339846933284976f4f3dbc397a515560047789c12f7e88a338ba466" ;
	<https://w3id.org/nostr#sig> "ed2182a3cd1211a82c7481b5abd3689cdd7a43938c88e41dbc742920a25918b9962be48e6f6a4d6b47d655eda08bb73322644de02fdee00939f0c4bc2d8f5627" .
<urn:nostr-content:a936e0a65c1d0d244d02f07b7f720ddf0910e253c219a7755bca3eff955d138a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://nostralink.org/Note> ;
	<http://nostralink.org/headline> "Bonjour" ;
	<http://nostralink.org/inLanguage> <urn:nostr-lang:fr-FR> ;
	<http://nostralink.org/commonMarkBody> "Une petite note" .
<urn:nostr-lang:fr-FR> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://nostralink.org/Language> ;
	<http://nostralink.org/name> "French (France)" ;
	<http://nostralink.org/alternateName> "fr-FR" ;
	<http://nostralink.org/bcp47> "fr-FR" .

Convert an event from stdin

Use the convert command to read an event as JSON from stdin, and convert it to RDF.

cat event.json | nostra convert