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

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:
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.

Please let me know if you have any questions or things are not clear from this post! As a creative webdeveloper I'm always improving my explainer skills as I know I'm often too technical for most of us.
Great write-up ! 🔥
reply