Run a Nostr relay with your own policies

dinsdag 2 april 2024 - 2093 woorden, 11 min read

My goal was to find an easy to install, use and configure Nostr relay implementation with custom policies. For example I would like to set a policy where I configure specific event kinds to be transmitted by the relay.

Currently, I’m running relays with Nostream (written with TypeScript) for nostr.sebastix.dev and Chorus (written with Rust) for relay.sebastix.social.

Another relay implementation was Jingle which was on my list to give it a try. I really liked the idea that you can write your own policies in JavaScript, because every webdeveloper could use it. In this blog I’ve shared my experience setting this relay up.
TLDR
Jingle is not working, so I’ve set up a relay with Khatru.

Run Jingle with JavaScript written policies

fiatjaf/jingle: a friendly customizable Nostr relay

With Jingle, by default, all data is stored in a data directory in a SQLite database. This is how my Nginx config file looks for running Jingle behind a reverse proxy setup:

upstream jingle {
    server 127.0.0.1:5577;
}

server {
    server_name  jingle.nostrver.se;

    location / {
        proxy_pass http://jingle;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
    }

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/jingle.nostrver.se/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/jingle.nostrver.se/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    access_log /var/log/nginx/jingle.nostrver.se.access.log;
    error_log /var/log/nginx/jingle.nostrver.se.error.log;
}

server {
    if ($host = jingle.nostrver.se) {
        return 301 https://$host$request_uri;
    }

    listen       80;
    server_name  jingle.nostrver.se;
    return 404;
}

Create your own relay policy with JavaScript

The JavaScript files are located in the stuff directory. These are the default JavaScript files (these are generated when the binary is build) installed to give you an idea what’s possible.

  • reject-event.js (called for every EVENT message)
  • reject-filter.js (called for every REQ message)

The default code of these JavaScript files comes from the reject.go file.

Use NAK as your Nostr client to test

When you’re developing stuff with Nostr, please have a look at nak - a command line tool for doing all things nostr. After you’ve git cloned this repository, run go build to create a nak binary. Now you can execute the following commands with the binary.

./nak —-help

NAME:
   nak - the nostr army knife command-line tool

USAGE:
   nak [global options] command [command options] [arguments...]

COMMANDS:
   req      generates encoded REQ messages and optionally use them to talk to relays
   count    generates encoded COUNT messages and optionally use them to talk to relays
   fetch    fetches events related to the given nip19 code from the included relay hints
   event    generates an encoded event and either prints it or sends it to a set of relays
   decode   decodes nip19, nip21, nip05 or hex entities
   encode   encodes notes and other stuff to nip19 entities
   key      operations on secret keys: generate, derive, encrypt, decrypt.
   verify   checks the hash and signature of an event given through stdin
   relay    gets the relay information document for the given relay, as JSON
   bunker   starts a NIP-46 signer daemon with the given --sec key
   help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --quiet, -q  do not print logs and info messages to stderr, use -qq to also not print anything to stdout (default: false)
   --help, -h   show help

Publish an event to relay(s) with authentication

./nak event --sec <your_nsec_in_hex_value> -c <content> --auth
More info about the event command, run ./nak event --help

NAME:
   nak event - generates an encoded event and either prints it or sends it to a set of relays

USAGE:
   nak event [command options] [relay...]

DESCRIPTION:
   outputs an event built with the flags. if one or more relays are given as arguments, an attempt is also made to publish the event to these relays.

   example:
       nak event -c hello wss://nos.lol
       nak event -k 3 -p 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d

   if an event -- or a partial event -- is given on stdin, the flags can be used to optionally modify it. if it is modified it is rehashed and resigned, otherwise it is just returned as given, but that can be used to just publish to relays.

   example:
       echo '{"id":"a889df6a387419ff204305f4c2d296ee328c3cd4f8b62f205648a541b4554dfb","pubkey":"c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5","created_at":1698623783,"kind":1,"tags":[],"content":"hello from the nostr army knife","sig":"84876e1ee3e726da84e5d195eb79358b2b3eaa4d9bd38456fde3e8a2af3f1cd4cda23f23fda454869975b3688797d4c66e12f4c51c1b43c6d2997c5e61865661"}' | nak event wss://offchain.pub
       echo '{"tags": [["t", "spam"]]}' | nak event -c 'this is spam'

OPTIONS:
   --auth              always perform NIP-42 "AUTH" when facing an "auth-required: " rejection and try again (default: false)
   --connect value     sign event using NIP-46, expects a bunker://... URL
   --connect-as value  private key to when communicating with the bunker given on --connect (default: a random key)
   --envelope          print the event enveloped in a ["EVENT", ...] message ready to be sent to a relay (default: false)
   --nevent            print the nevent code (to stderr) after the event is published (default: false)
   --nson              encode the event using NSON (default: false)
   --prompt-sec        prompt the user to paste a hex or nsec with which to sign the event (default: false)
   --sec value         secret key to sign the event, as hex or nsec (default: the key '1')

   EVENT FIELDS

   --content value, -c value                        event content (default: hello from the nostr army knife)
   --created-at value, --time value, --ts value     unix timestamp value for the created_at field (default: now)
   --kind value, -k value                           event kind (default: 1)
   --tag value, -t value [ --tag value, -t value ]  sets a tag field on the event, takes a value like -t e=<id>
   -d value [ -d value ]                            shortcut for --tag d=<value>
   -e value [ -e value ]                            shortcut for --tag e=<value>
   -p value [ -p value ]                            shortcut for --tag p=<value>

Request / query data from relays

./nak req
More info about the req command, run ./nak req --help

NAME:
   nak req - generates encoded REQ messages and optionally use them to talk to relays

USAGE:
   nak req [command options] [relay...]

DESCRIPTION:
   outputs a NIP-01 Nostr filter. when a relay is not given, will print the filter, otherwise will connect to the given relay and send the filter.

   example:
       nak req -k 1 -l 15 wss://nostr.wine wss://nostr-pub.wellorder.net
       nak req -k 0 -a 3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d wss://nos.lol | jq '.content | fromjson | .name'

   it can also take a filter from stdin, optionally modify it with flags and send it to specific relays (or just print it).

   example:
       echo '{"kinds": [1], "#t": ["test"]}' | nak req -l 5 -k 4549 --tag t=spam wss://nostr-pub.wellorder.net

OPTIONS:
   --auth              always perform NIP-42 "AUTH" when facing an "auth-required: " rejection and try again (default: false)
   --bare              when printing the filter, print just the filter, not enveloped in a ["REQ", ...] array (default: false)
   --connect value     sign AUTH using NIP-46, expects a bunker://... URL
   --connect-as value  private key to when communicating with the bunker given on --connect (default: a random key)
   --prompt-sec        prompt the user to paste a hex or nsec with which to sign the AUTH challenge (default: false)
   --sec value         secret key to sign the AUTH challenge, as hex or nsec (default: the key '1')
   --stream            keep the subscription open, printing all events as they are returned (default: false, will close on EOSE)

   FILTER ATTRIBUTES

   --author value, -a value [ --author value, -a value ]  only accept events from these authors (pubkey as hex)
   --id value, -i value [ --id value, -i value ]          only accept events with these ids (hex)
   --kind value, -k value [ --kind value, -k value ]      only accept events with these kind numbers
   --limit value, -l value                                only accept up to this number of events (default: 0)
   --search value                                         a NIP-50 search query, use it only with relays that explicitly support it
   --since value, -s value                                only accept events newer than this (unix timestamp)
   --tag value, -t value [ --tag value, -t value ]        takes a tag like -t e=<id>, only accept events with these tags
   --until value, -u value                                only accept events older than this (unix timestamp)
   -d value [ -d value ]                                  shortcut for --tag d=<value>
   -e value [ -e value ]                                  shortcut for --tag e=<value>
   -p value [ -p value ]                                  shortcut for --tag p=<value>

Fetch events

./nak fetch
More info about the fetch command, run ./nak fetch --help

NAME:
   nak fetch - fetches events related to the given nip19 code from the included relay hints

USAGE:
   nak fetch [command options] [nip19code]

DESCRIPTION:
   example usage:
           nak fetch nevent1qqsxrwm0hd3s3fddh4jc2574z3xzufq6qwuyz2rvv3n087zvym3dpaqprpmhxue69uhhqatzd35kxtnjv4kxz7tfdenju6t0xpnej4
           echo npub1h8spmtw9m2huyv6v2j2qd5zv956z2zdugl6mgx02f2upffwpm3nqv0j4ps | nak fetch --relay wss://relay.nostr.band

OPTIONS:
   --relay value, -r value [ --relay value, -r value ]  also use these relays to fetch from
   --help, -h                                           show help

You can also use NAK in the browser here: https://nak.nostr.com/

Let’s test and debug!

  1. Open a terminal where you can use the nak binary with ./nak
  2. For this demo I created the following secret key by executing ./nak key generate which returned a hex formatted secret key: 472f1868bebd7b8016534df94f8421c9b68c66c1914ccf9a99ca5d557f707a8b
  3. Open one of the JavaScript files from your Nostr relay with Jingle to make some edits.

This is my setup in my PHPStorm editor:

dhibNxf7

By default, the relay requires authentication of clients (seen in the screenshot above) which in configured in the stuff/reject.filter.js file. To authenticate with nak to the relay, you must send your private key (nsec) together with the --auth option. So ./nak req -k 30023 -l 1 --sec <put_your_nsec_here> --auth wss://jingle.nostrver.se should response with something like:

connecting to wss://jingle.nostrver.se... ok.
{"id":"0faeb0c150b9f370b702d4357de3536a7fd606be...a9779b6e2e957e26af557","pubkey":"efbb28950ec699e1f988dc8dba00e70cb89d18d7d9e931036a4c36ea4de77586","created_at":1711324345,"kind":30023,"tags":[],"content":"hello world","sig":"aef587a768298abeff08bdf7ef5eb0e84d93a0ef9e4fcdd162f9f3eff3cf3a35384c2054be76a9bdda303ca3de0ebcad...8a07221c01b8bd41da6be158edbbe"}

Use console.log() to debug your JavaScript. Depending on your setup, you can find the output in the server logs. In my case I had to dig into the logs of the docker container. You can also run the Jingle binary to see the direct output on your command-line.

Stuck…look like Jingle is broken for me

When the relay is returning a failed: msg: blocked: error applying policy script message, your JavaScript policy files are not valid. Please note that a fork of buke/quickjs-go: Go bindings to QuickJS is used (https://github.com/fiatjaf/quickjs-go) for parsing the JavaScript files. The ES2020 specification is used in that library.

After some more debugging and working out my own filters, it seems that Jingle is crashing randomly while returning unexpected token errors. See this issue and a short screencast how it occurs on my setup: https://shares.sebastix.dev/GBkBnfCu.mp4. I showed it to Fiatjaf as well, but he will need to make time to investigate this unexpected behavior of the relay. I suspect there is something going wrong in how the JavaScript code are being parsed / compiled in the Go runtime…

Khatru as a temporary alternative

While I entered a new rabbit hole, I forked fiatjaf/khatru. This is my forked repo, so you can check out my work in progress: https://github.com/sebastix/khatru.

Hodlbod (Jon) tipped me to have a look at coracle-social/triflector: A relay which enforces authentication based on custom policy which he build with Khatru.

As I’ve never written any line Golang code…I needed to learn some basics first.

  • How to install Go - see Download and install - The Go Programming Language
  • I quickly walked through this tour: A Tour of Go
  • Ho to run Go code: go run <your_file.go>
  • How to run and debug Go code: with Delve found at Go (Golang) Debugging Approaches. Now I could use dlv debug <your_file.go> but I haven’t found a way to debug with ease on the CLI.
  • How to build a binary from the code: go build <your_file.go>
  • Use a FOSS IDE for writing Go: still looking for one… I could use GoLand as I’m used to work with PHPStorm from JetBrains.

Khatru contains several Go files in the project root and some example snippets in the examples directory. I’m using the basic-sqlite example as a base for the work-in-progress relay setup I’m working out. Here you can view the current main.go file which is used for the relay.

You could connect to the relay at wss://khatru.nostrver.se .

$ ./nak relay wss://khatru.nostrver.se
{
  "name": "khatru.nostrver.se",
  "description": "Custom relay build with Khatru",
  "pubkey": "npub1qe3e5wrvnsgpggtkytxteaqfprz0rgxr8c3l34kk3a9t7e2l3acslezefe",
  "contact": "info@sebastix.nl",
  "supported_nips": [
    1,
    11,
    70
  ],
  "software": "<https://github.com/fiatjaf/khatru>",
  "version": "0.0.1",
  "icon": ""
}

As for now, the relay only accepts event with kind 37515 and 13811 as you can read on https://khatru.nostrver.se/.

Khatru is using a lot of packages from the Golang library for Nostr: nbd-wtf/go-nostr: Nostr library for Golang and is worth checking out as one of the most complete libraries for Nostr out there

I’ve also created a system daemon service for running the relay in the background on my server:

[Unit]
Description=khatru
After=network-online.target

[Service]
Type=simple
WorkingDirectory=/var/www/khatru.nostrver.se
User=sebastix
ExecStart=/usr/local/go/bin/go run examples/basic-sqlite3/main.go
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

A for now, the relay is running and only accepting Nostr event kinds 37515 and 13811. With this working setup, I’m able to continue building a proof-of-concept client around places and place check-ins 🫡.

Let me just draw a line here for the first chapter of exploring something new. I’m sure things will continue to evolve! Make sure to follow me on Nostr to keep up-to-date with my tinkering.


Sebastian Hagens @Sebastix
I work as creative webdeveloper & tech consultant and care about digital freedoms. Follow me:
or visit my contact page