# nostr-rs-relay and what running a Nostr relay actually involves

> nostr-rs-relay is a Rust implementation of the Nostr relay protocol with SQLite persistence and experimental PostgreSQL support. It covers nearly the whole NIP set, ships as a container image, and asks the operator to make several decisions the protocol leaves open, such as rate limits and event size caps.

**scsibug/nostr-rs-relay** — Mirror of https://sr.ht/~gheartsfield/nostr-rs-relay/

- Repository: https://github.com/scsibug/nostr-rs-relay
- Stars: 724 · Forks: 200
- Language: Rust
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/scsibug-nostr-rs-relay

## A relay that implements the whole protocol, with SQLite as the default

nostr-rs-relay is a Nostr relay written in Rust. The README states that it currently supports the entire relay protocol and persists data with SQLite, with experimental support for PostgreSQL.

That first sentence is a strong claim and the NIP checklist in the README is the evidence for it. The implementation covers the core event model, contact lists and petnames, DNS-based key mapping, event deletion, the relay information document, generic tag queries, the end-of-stored-events notice, event treatment, command results, `created_at` limits, public chat, parameterized replaceable events, expiration timestamps, client authentication, and the AND operator for filters.

Two entries in the checklist are the interesting part. NIP-03, OpenTimestamps attestations for events, is not supported. NIP-26, event delegation, is marked as implemented but currently disabled, which is a different failure mode from not being implemented, since the code exists and simply is not switched on. NIP-22 is limited to future-dated events only.

Persistence is SQLite through `rusqlite` with a connection pool, and PostgreSQL is available through `sqlx` with the postgres, chrono and runtime-tokio-rustls features. The word experimental in the README is doing real work there, and it is the constraint most likely to matter to a serious deployment.

## The GitHub copy is a mirror, and the master lives on sourcehut

The repository you are reading is not the project's home. The README states that the project master repository is available on sourcehut, and that the GitHub repository is a mirror. The repository description says the same thing in one line.

For most users that is a detail. For anyone planning to contribute, to follow releases, or to file an issue, it decides where the work goes. The build badge in the README points at builds.sr.ht for the master branch, while a separate GitHub CI badge covers the mirror, so the authoritative continuous integration runs on sourcehut.

There is a second-order effect too. The Cargo manifest points `homepage` and `repository` at the sourcehut URLs, and the `default-run` is `nostr-rs-relay`. A developer following the manifest will clone from sourcehut, which is where the README's build instructions tell you to clone from as well.

The mirror arrangement is a reasonable choice for a project that values sourcehut, and it costs an outsider a small amount of confusion: the repository you find through a search engine is the copy, and the copy carries the same README, so nothing in the file itself tells you that the canonical history lives elsewhere.

## The container runs as a non-root user with the database on a bind mount

The quick start builds the server with the provided Dockerfile and then runs it so that the SQLite database lives outside the container image. The examples use rootless podman, mapping the container's 8080 port to a host port:

```console
$ podman build --pull -t nostr-rs-relay .

$ mkdir data

$ podman unshare chown 100:100 data
```

The `unshare chown` step is what makes a rootless container able to write to the host directory, since the container user maps to a subuid on the host. The run command then wires the data directory, the config file and the port together:

```console
$ podman run -it --rm -p 7000:8080 \
  --user=100:100 \
  -v $(pwd)/data:/usr/src/app/db:Z \
  -v $(pwd)/config.toml:/usr/src/app/config.toml:ro,Z \
  --name nostr-relay nostr-rs-relay:latest
```

The `:Z` suffix relabels the bind mount for SELinux, and `:ro` makes the config read only. The port mapping means the relay is reachable on 7000 on the host while it listens on 8080 inside. A pre-built image is also published on Docker Hub under the `scsibug/nostr-rs-relay` repository, so building locally is optional.

The startup log shows the sequence to expect: the server binds `0.0.0.0:8080`, a database writer is created, a control message listener starts, a connection pool for the event writer is built, the database is opened for writing, and the schema is initialised with its migration scripts running to completion.

## Building from source is three commands, and the binary listens on 8080

Building without Docker needs Cargo and Rust, and the README lists the OS packages that help. On Debian and Ubuntu that is `build-essential cmake protobuf-compiler pkg-config libssl-dev`, and on OpenBSD it is `doas pkg_add rust protobuf`.

The build itself is short:

```console
$ git clone -q https://git.sr.ht/\~gheartsfield/nostr-rs-relay
$ cd nostr-rs-relay
$ cargo build -q -r
```

The executable lands at `target/release/nostr-rs-relay`. Running it with logging enabled uses the `RUST_LOG` variable:

```console
$ RUST_LOG=warn,nostr_rs_relay=info ./target/release/nostr-rs-relay
```

The log lines that follow show the same subsystems as the container run, plus one that only appears on a native build: a maintenance writer connection pool, and a reported database version of 11 with the schema left at an earlier version, since a native run does not necessarily start from an empty database.

The connection pool sizes are also visible and worth noting. The client query pool is built with a minimum of 4 and a maximum of 128 in the container run, and a maximum of 8 in the native run, while the event writer pool is much smaller. Those numbers are how the relay bounds its database concurrency, and they are not obviously configurable from the config file the README describes.

## The Dockerfile produces an auditable binary and drops privileges

The container build is worth reading because it does two things that matter for supply chain review. It installs `cargo-auditable` and uses `cargo auditable build --release --locked` twice, once to warm the dependency cache and once for the real build after the project source is copied in. The `--locked` flag pins the dependency graph to `Cargo.lock`, and the auditable build embeds the dependency list in the binary.

The multi-stage layout keeps the build tooling out of the runtime image. The builder stage uses `rust:1-bookworm` and needs `cmake` and `protobuf-compiler` for the protobuf definitions in the `proto/` directory, which the `tonic` and `prost` dependencies generate code from. The runtime stage is `debian:bookworm-slim` with `ca-certificates`, `tzdata`, `sqlite3` and `libc6`.

The runtime image then creates a dedicated user and gives up root:

```dockerfile
EXPOSE 8080

ENV TZ=Etc/UTC \
    APP_USER=appuser
```

and the container is started with the data directory passed in:

```dockerfile
CMD ./nostr-rs-relay --db ${APP_DATA}
```

A relay that accepts public writes and holds a database is exactly the kind of service that should not run as root in production, and this image does not.

## config.toml is where you decide what your relay will accept

The sample `config.toml` file demonstrates the configuration available to the relay, and the README is clear that the file is optional. It can be mounted into a container as a bind mount alongside the data directory and an `index.html` file:

```console
$ docker run -it -p 7000:8080 \
  --mount src=$(pwd)/config.toml,target=/usr/src/app/config.toml,type=bind \
  --mount src=$(pwd)/data,target=/usr/src/app/db,type=bind \
  --mount src=$(pwd)/index.html,target=/usr/src/app/index.html,type=bind \
  nostr-rs-relay
```

The README names the option groups: rate limiting, event size limits, and network address settings. The dependency list shows the machinery behind at least the first of them, since `governor` is a rate limiting library and `config` with the toml feature loads the file.

Those three groups are the entire operator surface, and they are the decisions that separate one relay from another. A relay that accepts a megabyte event and a relay that caps events at a few kilobytes will behave very differently under load and will also be indexed very differently by clients. The README does not enumerate every key or state the defaults, so reading the sample file is the only way to know what a given deployment will accept.

For TLS and load balancing, the README points to a reverse proxy document under `docs/`.

## Operating a relay means watching the schema version and the metrics

Running a relay involves more than a start command. The startup logs show a schema version and a set of migration scripts that run on startup, and the values in the two documented runs differ, from schema v7 in the container example to a reported database version of 11 in the native one. That is the migration system doing its job, and it is also the thing to watch after a version bump.

The dependency list also shows that the relay exports Prometheus metrics through the `prometheus` crate and has a `console-subscriber` dependency alongside `tracing`, `tracing-subscriber` and `tracing-appender` for logging. The `hyper` and `hyper-rustls` combination with `tokio-tungstenite` and `tungstenite` covers the HTTP and websocket sides, and `clap` with the env feature is how the command line and environment are parsed.

On maintenance, the repository is not archived and the last push was on 2026-05-22. There are no GitHub releases, and the version in the Cargo manifest reads 0.10.0. The licence is MIT, declared in the manifest and with the text in the LICENSE file, and the crate publishes categories for network programming and web programming.

The repository also carries a Nix flake, a `.build.yml` for sourcehut builds, a `.pre-commit-config.yaml`, a `contrib/` directory, `tests/`, `docs/` and an `examples/nauthz/` directory, which suggests the authorisation flow has a worked example.

## Conclusion

nostr-rs-relay fits an operator who wants a self-hosted Nostr relay they can configure, inspect and build from source, and who is comfortable with SQLite as the store and with a container that ships its own defaults. It is the wrong choice for a deployment that needs PostgreSQL as a first class backend, since that support is described as experimental, and for anyone who needs NIP-03 or NIP-26, which are either unsupported or disabled. Verify first that the `config.toml` you mount sets the rate limits and event size limits appropriate to your clients, and that the SQLite database lives on a bind mount outside the container, because the relay writes to `/usr/src/app/db`.

## FAQ

### How do I run nostr-rs-relay with Docker or podman?

The README builds the image with `podman build --pull -t nostr-rs-relay .` and runs it with a bind mount for the database directory, a read only mount for `config.toml` and a port mapping from 8080 in the container to 7000 on the host. A pre-built image is also available on Docker Hub.

### Which NIPs does nostr-rs-relay support?

The README checklist marks NIP-01, 02, 05, 09, 11, 12, 15, 16, 20, 22, 28, 33, 40, 42 and 91 as implemented. NIP-03 is not supported, NIP-26 is implemented but currently disabled, and NIP-22 covers future-dated events only.

### Can nostr-rs-relay use PostgreSQL instead of SQLite?

PostgreSQL support is described as experimental in the README, while SQLite is the default store, and the `sqlx` dependency carries the postgres, chrono and runtime-tokio-rustls features.

### Where is the configuration for a nostr-rs-relay deployment?

The sample `config.toml` at the repository root demonstrates the available options, which the README lists as rate limiting, event size limits and network address settings. The file is optional and can be mounted into the container at `/usr/src/app/config.toml`.

### Where is the master repository for nostr-rs-relay?

The README states that the project master repository is on sourcehut at `https://sr.ht/~gheartsfield/nostr-rs-relay/` and that the GitHub repository is a mirror. The Cargo manifest also points its `homepage` and `repository` fields at the sourcehut URLs.

## Sources

- [Issues](https://github.com/scsibug/nostr-rs-relay/issues)
- [License: MIT](https://github.com/scsibug/nostr-rs-relay/blob/master/LICENSE)
- [README](https://github.com/scsibug/nostr-rs-relay/blob/master/README.md)
- [scsibug/nostr-rs-relay on GitHub](https://github.com/scsibug/nostr-rs-relay)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/scsibug-nostr-rs-relay
