ephemeral-relay

command module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 12, 2026 License: MIT Imports: 22 Imported by: 0

README

Ephemeral Relay

Events are not forever.

CI Go Built on khatru NIPs

Ephemeral Relay is a Nostr relay for temporary events. Built for content whose natural lifetime is hours, not years — e.g. live-stream chat, bridged messages, presence — built on the Khatru framework.

The relay tells clients its policy itself. Its NIP-11 description is generated from your acceptance and retention settings, so you don't have to trust a README:

Accepts kinds 0,5,7,16,1311,1312,1313,9735,10312 only. All events except kinds 0 are deleted after 3h0m0s.

The life of an event

flowchart LR
    A([EVENT arrives]) -->|kind not in ALLOWED_KINDS| X([rejected])
    A -->|NIP-40 tag expired| X
    A -->|NIP-70 “-” tag, sender not authed as author| X
    A --> S[(stored & served)]
    S -->|NIP-40 expiration passes| H[hidden from queries] --> D([deleted by sweep])
    S -->|older than RETENTION_SECONDS| D
    S -.->|kind in RETENTION_EXEMPT_KINDS| K([kept indefinitely])
    H ~~~ K

Protected three ways

The posture of this relay is ephemeral, limited, anti-gossip. For content that must not outlive its moment — say, chat bridged from users on another platform who never signed up for permanent, globally-replicated speech. It is intended to be used together with events holding NIP-40 and NIP-70 tags.

  1. Blanket TTL — the relay hard-deletes every retained-kind event after RETENTION_SECONDS.
  2. NIP-40 expiration — the relay respects NIP-40 expiration times, and recommends it, so that even if a copy escapes to other relays, honest ones delete it on schedule.
  3. NIP-70 protected events — the relay respects NIP-70 protected events, validates them with NIP-42 and recommends their use so that an honest relay will not accept the event from anyone but its author and a rebroadcast copy is never stored at all.

The ephemeral anti-gossip posture is that this relay will delete events after RETENTION_SECONDS, and that all other honest relays should not store the events at all (NIP-70), and should delete them at expiry if stored (NIP-40).

Why this relay

No off-the-shelf relay combined these (as of mid-2026):

nostr-rs-relay strfry WoT Relay (khatru) Ephemeral Relay
Age-based retention Days-grain
Kind allowlist Plugin
NIP-40 honoured
NIP-70 enforced

Prove it in 60 seconds

With docker and nak:

docker compose up -d --build   # relay on ws://localhost:7448

# publish a chat message that expires in 30 seconds
nak event -k 1311 -c "I will expire" -t expiration=$(($(date +%s)+30)) ws://localhost:7448

nak req -k 1311 ws://localhost:7448   # there it is
sleep 30
nak req -k 1311 ws://localhost:7448   # gone

Use Cases

  • Live stream chat: chat messages, reactions, reposts and zap receipts live for the duration of a show and then scroll away, like chat is supposed to.
  • Bridged content: colocate with a bridge that republishes users from another platform; stamp its events with expiration and - to enforce transience across an honest network.
  • Ephemeral notice boards: announcements, presence, status — anywhere stale content is worse than no content.

The default kind set is live-chat flavoured, but it's entirely yours to configure — set ALLOWED_KINDS to whatever your use case needs. The only opinion this relay keeps is that things expire.

Prerequisites

  • Go: Ensure you have Go installed on your system. You can download it from here.
  • Build Essentials: a C compiler is required (the LMDB storage backend uses cgo). On Linux: sudo apt install build-essential.

Setup Instructions

1. Clone the repository
git clone https://github.com/r0d8lsh0p/ephemeral-relay.git
cd ephemeral-relay
2. Copy .env.example to .env
cp .env.example .env
3. Set your environment variables
Variable Default Meaning
ALLOWED_KINDS 0,5,7,16,1311,1312,1313,9735,10312 Only these kinds are accepted — configure for your use case. Default covers profiles, deletions, reactions, reposts, live chat / raids / clips (1311/1312/1313, per zap.stream), zap receipts, and NIP-53 room presence
RETENTION_SECONDS 10800 (3 h) Events older than this are hard-deleted
PURGE_INTERVAL_SECONDS 600 How often the deletion sweep runs
RETENTION_EXEMPT_KINDS 0 Kinds kept indefinitely (profiles by default)
RATE_LIMIT_EVENTS_PER_SEC / RATE_LIMIT_BURST 10 / 50 Per-IP write rate limit
TRUSTED_IPS IPs exempt from the rate limit
DEMAND_ENDPOINT false Enable GET /demand (open-subscription introspection)
DEMAND_KINDS — (any) Only track subscriptions asking for these kinds
DEMAND_STALE_SECONDS 600 How long a zero-subscriber entry stays in the response
AUTH_TOKEN If set, gated HTTP endpoints (currently /demand) require Authorization: Bearer <token>
PORT 3335 Listen port
DB_PATH db/ LMDB database path
RELAY_NAME / RELAY_PUBKEY / RELAY_ICON / RELAY_CONTACT / RELAY_DESCRIPTION NIP-11 identity (description auto-generated from retention settings unless set)

Some values are fixed on purpose:

Fixed Value Why it isn't an env var
Ephemeral-range wildcard off khatru's kind policy can blanket-admit all of NIP-01's ephemeral range (20000–29999); that would undercut the limited posture. Want a specific ephemeral kind? Put its number in ALLOWED_KINDS.
Timestamp sanity window 2 h past / 30 min future Events dated outside this window are rejected so that back-dating or forward-dating cannot be used to avoid deletion.

[!IMPORTANT] NIP-40 can only shorten an event's life. A distant expiration does not exempt an event from RETENTION_SECONDS — the blanket window always wins.

4. Build the project
go build
5. Create a Systemd Service (optional)
  1. Create the file:
sudo nano /etc/systemd/system/ephemeral-relay.service
  1. Add the following contents:
[Unit]
Description=Ephemeral Relay Service
After=network.target

[Service]
ExecStart=/home/ubuntu/ephemeral-relay/ephemeral-relay
WorkingDirectory=/home/ubuntu/ephemeral-relay
Restart=always

[Install]
WantedBy=multi-user.target

Replace /home/ubuntu/ with the actual path where you cloned the repository.

  1. Reload systemd, start, and (optionally) enable on boot:
sudo systemctl daemon-reload
sudo systemctl start ephemeral-relay
sudo systemctl enable ephemeral-relay

[!TIP] If the relay can't read or write its database, give the service user ownership of the db folder — sudo chown -R <service-user> /path/to/db — rather than opening permissions wide.

6. Serving over nginx (optional)
server {
    listen 80;
    server_name chat.yourdomain.com;

    location / {
        proxy_pass http://localhost:3335;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Replace chat.yourdomain.com with your actual domain name, then restart nginx:

sudo systemctl restart nginx

[!WARNING] X-Forwarded-For is load-bearing: the rate limiter identifies clients by real IP through the proxy, and TRUSTED_IPS entries are matched against those. Strip or mangle it and every client looks like your proxy.

7. Install Certbot (optional)

If you want to serve the relay over HTTPS, use Certbot to generate an SSL certificate:

sudo apt-get update
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx
8. Access the relay

Once everything is set up, the relay will be running on localhost:3335 or your domain name if you set up nginx. Fetch its NIP-11 document to confirm the policy it's advertising:

curl -H 'Accept: application/nostr+json' https://chat.yourdomain.com

Start the Project with Docker Compose

  1. Ensure Docker and Docker Compose are installed on your system.

  2. Adjust the environment variables in docker-compose.yml as needed.

  3. Run:

    # in foreground
    docker compose up --build
    # in background
    docker compose up --build -d
    
  4. To update the relay:

    git pull
    docker compose build --no-cache
    docker compose up -d
    

The relay service will be accessible on port 7448 (mapped from the container's 3335).

Bots and bridges

If you run your own software against this relay — such as a bot or a bridge — two features help. Neither changes behaviour for ordinary clients.

TRUSTED_IPS — exempt your own services from the rate limit

A bridge relaying fifty chat rooms arrives from one IP and would be throttled like one very chatty client. List its address in TRUSTED_IPS to exempt it. Everyone else stays metered. On a platform like Railway, whitelist your service's private-network address, which is stable.

GET /demand — see what clients are subscribed to

With DEMAND_ENDPOINT=true, the relay reports the REQs currently open on it, so your service can do expensive work only while someone is listening. For example, a bridge joins a third-party chat room only while some client is subscribed to that room's chat.

curl -s -H "Authorization: Bearer $AUTH_TOKEN" http://localhost:3335/demand
# {"demand":[{"filter":{"kinds":[1311],"#a":["30311:<pubkey>:<d>"]},"active":2,"last_seen":"..."}]}

The response reveals what clients are subscribed to — on a public relay, leave the endpoint off, set AUTH_TOKEN, or gate it at your proxy.

Tests

Unit and in-process integration tests (go test ./...) cover the policies, purge logic, rate limiter (with an injected clock), and a full websocket relay round-trip on the in-memory slicestore — they run in well under a second. CI runs them plus the protocol-level e2e below on every push.

End-to-End Tests

A protocol-level checker lives in e2e/. Point it at a relay running with short retention:

RETENTION_SECONDS=8 PURGE_INTERVAL_SECONDS=2 PORT=3336 DB_PATH=/tmp/eph-e2e ./ephemeral-relay &
go run ./e2e -relay ws://localhost:3336 -retention 8

Checks: disallowed kinds rejected; chat accepted and served; after the retention window chat is purged while a kind 0 profile survives.

Additional modes:

go run ./e2e -relay ws://localhost:3336 -nip40 -ttl 180 # short-TTL event dies on time, tagless control survives
go run ./e2e -relay ws://localhost:3336 -nip70 # "-" events: author-only publish, rebroadcasts refused
go run ./e2e -relay ws://localhost:3336 -burst-only # rate limiter caps an untrusted burst
go run ./e2e -relay ws://localhost:3336 -burst-only -burst-trusted # TRUSTED_IPS bypass takes the full volley

[!IMPORTANT] Run the two burst checks against a locally-run binary, as shown — the relay must see the client arrive from 127.0.0.1. Behind docker port-mapping the client appears as the bridge gateway IP instead, which tests a different (weaker) case: the untrusted check can pass even if localhost traffic were accidentally exempt. Run as a pair on the same relay: the untrusted check proves localhost is not exempt, which is what makes the trusted check's 80/80 meaningful.

License

MIT

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
End-to-end test against a running ephemeral-relay.
End-to-end test against a running ephemeral-relay.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL