A multi-tenant Nostr relay for communities.
Quick start ·
Configuration ·
API
---
Zooid is a multi-tenant relay built on [Khatru](https://gitworkshop.dev/fiatjaf.com/nostrlib/tree/master/khatru) with a flexible set of access controls. It's designed to pair with [Flotilla](https://flotilla.social) as a community relay (with full NIP 29 support), but it works just fine outside of a community context too.
## Features
- **Multi-tenant** — run any number of virtual relays from a single instance, each with its own host, schema, and policy.
- **Community-ready** — first-class support for [NIP 29](https://github.com/nostr-protocol/nips/blob/master/29.md) groups, invite codes, and role-based access.
- **Batteries included** — optional [Blossom](https://github.com/hzrd149/blossom) media, [NIP 86](https://github.com/nostr-protocol/nips/blob/master/86.md) management, [NIP 9a](https://github.com/nostr-protocol/nips/pull/1079) push, and [LiveKit](https://livekit.io/) audio/video calls.
- **Remotely manageable** — JSON REST API authenticated via [NIP 98](https://github.com/nostr-protocol/nips/blob/master/98.md).
- **Operationally simple** — single binary, SQLite storage, OCI container, optional pprof.
## Quick start
```sh
docker run -it \
-p 3334:3334 \
-v ./config:/app/config \
-v ./media:/app/media \
-v ./data:/app/data \
gitea.coracle.social/coracle/zooid
```
Drop a TOML config file into `./config/` (see [Configuration](#configuration)) and the relay will be available at `ws://:3334`.
## Architecture
A single zooid instance can run any number of "virtual" relays. The `config` directory can contain any number of configuration files, each of which represents a single virtual relay.
## Environment
Zooid supports a few environment variables, which configure shared resources like the web server or sqlite database.
- `PORT` - the port the server will listen on for all requests. Defaults to `3334`.
- `CONFIG` - where to store relay configuration files. Defaults to `./config`.
- `MEDIA` - where to store blossom media files. Defaults to `./media`.
- `DATA` - where to store database files. Defaults to `./data`.
- `API_HOST` - the hostname on which to expose the management API. If not set, the API is disabled.
- `API_WHITELIST` - a comma-separated list of nostr hex pubkeys authorized to use the management API. Required when `API_HOST` is set.
- `PPROF_ADDR` - an http host to serve pprof stats on.
## Configuration
Configuration files are written using [toml](https://toml.io). Top level configuration options are required:
- `host` - a hostname to serve this relay on. Must match the `Host` header sent by the client, including the port if the client connects on a non-standard port (e.g. `relay.example.com:8443`). When running behind a reverse proxy such as nginx, ensure the proxy forwards the original `Host` header unmodified; with nginx, use `proxy_set_header Host $http_host;` (the common `$host` variable drops the port).
- `schema` - a string that identifies this relay. This cannot be changed, and must be usable as a sqlite identifier.
- `secret` - the nostr secret key of the relay. Will be used to populate the relay's NIP 11 `self` field and sign generated events.
- `inactive` - a boolean indicating whether the relay is currently inactive. The relay will not be available if set.
### `[info]`
Contains information for populating the relay's `nip11` document.
- `name` - the name of your relay.
- `icon` - an icon for your relay.
- `pubkey` - the public key of the relay owner. Does not affect access controls.
- `description` - your relay's description.
### `[policy]`
Contains policy and access related configuration.
- `public_join` - whether to allow non-members to join the relay without an invite code. Defaults to `false`.
- `strip_signatures` - whether to remove signatures when serving events to non-admins. This requires clients/users to trust the relay to properly authenticate signatures. Be cautious about using this; a malicious relay will be able to execute all kinds of attacks, including potentially serving events unrelated to a community use case.
### `[groups]`
Configures NIP 29 support.
- `enabled` - whether NIP 29 is enabled.
### `[management]`
Configures NIP 86 support.
- `enabled` - whether NIP 86 is enabled.
### `[blossom]`
Configures blossom support.
- `enabled` - whether blossom is enabled.
- `authenticated_read` - whether users must perform NIP 98 AUTH in order to fetch a file.
- `adapter` - where to store blobs. Either `local` (the default, stores files under `MEDIA`) or `s3` (stores files in an S3-compatible bucket).
#### `[blossom.s3]`
Configures S3-compatible object storage, used when `blossom.adapter` is `s3`.
- `endpoint` - the S3 endpoint URL. Optional; leave unset to use AWS S3.
- `region` - the bucket region. Required when `adapter` is `s3`.
- `bucket` - the bucket name. Required when `adapter` is `s3`.
- `access_key` - the access key ID. Required when `adapter` is `s3`.
- `secret_key` - the secret access key. Required when `adapter` is `s3`.
- `key_prefix` - an optional prefix prepended to every object key.
### `[push]`
Configures NIP 9a push support.
- `enabled` - whether push is enabled.
### `[roles]`
Defines roles that can be assigned to different users and attendant privileges. Each role is defined by a `[roles.{role_name}]` header and has the following options:
- `pubkeys` - a list of nostr pubkeys this role is assigned to.
- `can_invite` - a boolean indicating whether this role can invite new members to the relay by requesting a `kind 28935` claim. Defaults to `false`. See [access requests](https://github.com/nostr-protocol/nips/pull/1079) for more details.
- `can_manage` - a boolean indicating whether this role can use NIP 86 relay management and administer NIP 29 groups. Defaults to `false`.
A special `[roles.member]` heading may be used to configure policies for all relay users (that is, pubkeys assigned to other roles, or who have redeemed an invite code).
### `[livekit]`
[Livekit](https://livekit.io/) is an open source WebSockets toolkit for audio and video calls. Configuring a livekit server allows clients to start audio and video calls.
- `server_url` - the URL to your Livekit server.
- `api_key` - a key identifying this relay, assigned by the Livekit server.
- `api_secret` - a secret key authenticating this relay, assigned by the Livekit server.
On your LiveKit server you should also set up a webhook that points to `https://yourrelay.com/.well-known/nip29/livekit/webhook`. This allows LiveKit to notify your relay when people join rooms so it can publish a kind 39004 event.
### Example
The below config file might be saved as `./config/my-relay.example.com` in order to route requests from `wss://my-relay.example.com:3334` to this virtual relay.
```toml
host = "my-relay.example.com:3334"
schema = "my_relay"
secret = ""
[info]
name = "My relay"
icon = "https://example.com/icon.png"
pubkey = ""
description = "A community relay for my friends"
[policy]
public_join = true
strip_signatures = false
[groups]
enabled = true
[management]
enabled = true
[blossom]
enabled = false
[push]
enabled = false
[roles.member]
can_invite = true
[roles.admin]
pubkeys = ["d9254d9898fd4728f7e2b32b87520221a50f6b8b97d935d7da2de8923988aa6d"]
can_manage = true
```
## API
When `API_HOST` and `API_WHITELIST` are configured, a JSON REST API is available for managing virtual relays remotely. All API requests must be authenticated using [NIP 98](https://github.com/nostr-protocol/nips/blob/master/98.md) HTTP AUTH.
The API accepts JSON config objects and stores them as TOML files in the `CONFIG` directory. Configs are validated for required fields (`host`, `schema`, `secret`) and duplicate checking (`schema` and `host` must be unique across all relays).
Endpoints:
- `POST /relay/{id}` - Creates a new virtual relay config. Returns 201 on success, 409 if the id/schema/host already exists, 400 for invalid config.
- `PUT /relay/{id}` - Updates an existing virtual relay config. Returns 200 on success, 404 if the id doesn't exist, 409 if the new schema/host conflicts with another relay.
- `PATCH /relay/{id}` - Partially updates an existing virtual relay config by recursively merging the provided JSON. Returns 200 on success, 404 if the id doesn't exist, 409 if the new schema/host conflicts, 400 for invalid config. Use `null` values to remove fields.
- `DELETE /relay/{id}` - Deletes a virtual relay config. Returns 200 on success, 404 if the id doesn't exist.
- `GET /relay/{id}/members` - Returns relay members as JSON (`{"members": ["", ...]}`). Returns 200 on success, 404 if the relay id does not exist.
## Scripts
After running `just build`, a number of scripts will be available:
- `./bin/export` prints JSONL events to stdout for a given virtual relay
- `./bin/import` takes JSONL events on stdin and imports it into the given virtual relay
## Development
See `justfile` for defined commands.
## License
[MIT](./LICENSE)