This is a fork of Zooid, a multi-tenant relay based on Khatru which implements a range of access controls. This fork is customized for use with Unicity Sphere for NIP-29 group chat functionality.
This fork includes the following changes from upstream Zooid:
Modified groups.go to allow authenticated users to read messages from public groups with open policy, without requiring explicit group membership:
// In CanRead function - allows public group access
if g.Config.Policy.Open && !HasTag(meta.Tags, "private") {
return true
}This enables the "browse and join" workflow where users can discover public groups, view their messages, and then join if interested.
Added configurable group creation policies and private group access control:
admin_create_only— only relay admins can create groupsprivate_admin_only— only relay admins can create private groups (public groups open to all)private_relay_admin_access— whenfalse, relay admins cannot see or moderate private groups; only the group creator can moderate their own private group
Added support for write-restricted groups (announcement channels). A group with the write-restricted metadata flag only allows designated writers, admins, and the group creator to post. Regular members can read but not write.
- Uses NIP-29 roles on put-user events (kind 9000) — the
writerrole designates who can post - Only relay admins can create or set
write-restrictedon groups - Roles are tracked in a separate in-memory cache alongside the membership cache
- The group members list (kind 39002) includes role information in p-tags
- Combine with
closedfor public announcement channels (anyone can join and read, only writers can post)
Create a write-restricted group via the groupchat CLI:
node create-group.js "Announcements" "Official updates" --write-restricted --writer <pubkey>
node manage-writers.js add announcements <pubkey>
node manage-writers.js remove announcements <pubkey>The relay is configured with:
public_join = true— allows anyone to join without invitegroups.enabled = true— enables NIP-29 supportgroups.auto_join = true— members can join groups without approval- Open policy for public group message access
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.
Zooid supports a few environment variables, which configure shared resources like the web server or PostgreSQL database.
DATABASE_URL- required. PostgreSQL connection string (e.g.,postgres://user:pass@host:5432/dbname?sslmode=verify-full).PORT- the port the server will listen on for all requests. Defaults to3334.CONFIG- where to store relay configuration files. Defaults to./config.MEDIA- where to store blossom media files. Defaults to./media.DB_MAX_OPEN_CONNS- maximum open database connections. Defaults to20.DB_MAX_IDLE_CONNS- maximum idle database connections. Defaults to5.DB_CONN_MAX_LIFETIME_SECS- connection max lifetime in seconds. Defaults to300.
Configuration files are written using toml. Top level configuration options are required:
host- a hostname to serve this relay on.schema- a string that identifies this relay. This cannot be changed, and must be usable as a SQL identifier (alphanumeric and underscores only).secret- the nostr secret key of the relay. Will be used to populate the relay's NIP 11selffield and sign generated events.
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.
Contains policy and access related configuration.
public_join- whether to allow non-members to join the relay without an invite code. Defaults tofalse.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.
Configures NIP 29 support.
enabled- whether NIP 29 is enabled.auto_join- whether relay members can join groups without approval. Defaults tofalse.admin_create_only- only relay admins can create groups. Defaults totrue.private_admin_only- only relay admins can create private groups. Defaults totrue.private_relay_admin_access- relay admins can see and moderate private groups. Whenfalse, only the group creator can moderate their private group. Defaults tofalse.
Groups also support a write-restricted metadata flag (set in the group creation content JSON). When set, only members with the writer role, relay admins, and the group creator can post. The writer role is assigned via kind 9000 (put-user) events with ["p", "<pubkey>", "writer"] tags. Only relay admins can create write-restricted groups or add the flag to existing groups.
Configures NIP 86 support.
enabled- whether NIP 86 is enabled.methods- a list of NIP 86 relay management methods enabled for this relay.
Configures blossom support.
enabled- whether blossom is enabled.
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 akind 28935claim. Defaults tofalse. See access requests for more details.can_manage- a boolean indicating whether this role can use NIP 86 relay management and administer NIP 29 groups. Defaults tofalse.
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).
The below config file might be saved as ./config/my-relay.example.com in order to route requests from wss://my-relay.example.com to this virtual relay.
host = "my-relay.example.com"
schema = "my_relay"
secret = "<hex private key>"
[info]
name = "My relay"
icon = "https://example.com/icon.png"
pubkey = "<hex public key>"
description = "A community relay for my friends"
[policy]
public_join = true
strip_signatures = false
[groups]
enabled = true
auto_join = false
[management]
enabled = true
methods = ["supportedmethods", "banpubkey", "allowpubkey"]
[blossom]
enabled = false
[roles.member]
can_invite = true
[roles.admin]
pubkeys = ["d9254d9898fd4728f7e2b32b87520221a50f6b8b97d935d7da2de8923988aa6d"]
can_manage = trueSee justfile for defined commands.
Zooid requires a PostgreSQL 16+ database. It can be run using an OCI container:
podman run -it \
-p 3334:3334 \
-e DATABASE_URL="postgres://zooid:password@db-host:5432/zooid?sslmode=verify-full" \
-v ./config:/app/config \
-v ./media:/app/media \
ghcr.io/coracle-social/zooidFor local development with Sphere, start the PostgreSQL database and the relay:
# Start PostgreSQL
docker compose up -d postgres
# Run the relay (requires DATABASE_URL)
DATABASE_URL="postgres://zooid:dev@localhost:5432/zooid?sslmode=disable" just runOr use Docker Compose with a full containerized setup:
cd /path/to/groupchat # Contains docker-compose.yml and config/
docker compose up -dThis starts Zooid on ws://localhost:3334 with the pre-configured localhost relay.
The config/localhost file provides a development configuration:
host = "localhost"
schema = "localhost"
secret = "<relay-private-key>"
[info]
name = "Localhost Relay"
description = "Local development NIP-29 relay for Sphere"
[policy]
public_join = true
[groups]
enabled = true
auto_join = true
[roles.member]
can_invite = trueCheck relay status:
curl http://localhost:3334View logs:
docker compose logs -f zooidThis fork includes automated CI/CD:
Build and Push (docker-build.yml):
- Triggers on push to main/master or tags
- Builds Docker image
- Pushes to GitHub Container Registry (
ghcr.io/unicitylabs/zooid) - Tags:
latest,sha-<commit>, semver tags
Deploy to AWS (deploy-aws.yml):
- Triggers after successful build or manual dispatch
- Forces new ECS deployment
- Waits for service stability
Add these to GitHub repository secrets for AWS deployment:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY
# Force new deployment
aws ecs update-service \
--cluster sphere-zooid-relay-cluster \
--service sphere-zooid-relay-zooid-relay \
--force-new-deployment \
--region me-central-1See /Users/pavelg/work/unicity/sphere-infra/aws/ for CloudFormation templates and deployment scripts.
When running in AWS ECS, these environment variables configure the relay:
| Variable | Description |
|---|---|
DATABASE_URL |
Required. PostgreSQL connection string (e.g., postgres://user:pass@host:5432/zooid?sslmode=verify-full) |
RELAY_HOST |
Domain name (e.g., sphere-relay.unicity.network) |
RELAY_SECRET |
Nostr private key (64-char hex) |
RELAY_NAME |
Display name |
RELAY_DESCRIPTION |
Description |
ADMIN_PUBKEYS |
Admin pubkeys (quoted, comma-separated) |
GROUPS_ADMIN_CREATE_ONLY |
Only admins can create groups (default: true) |
GROUPS_PRIVATE_ADMIN_ONLY |
Only admins can create private groups (default: true) |
GROUPS_PRIVATE_RELAY_ADMIN_ACCESS |
Relay admins can see/moderate private groups (default: false) |
DB_MAX_OPEN_CONNS |
Max open DB connections (default: 20) |
DB_MAX_IDLE_CONNS |
Max idle DB connections (default: 5) |
DB_CONN_MAX_LIFETIME_SECS |
Connection max lifetime in seconds (default: 300) |