No description
  • Go 99.5%
  • Dockerfile 0.5%
Find a file
@s.roertgen 62f90411b9
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m9s
Remove diagnostic relay logging after identifying Docker DNS collision
Root cause found: relay container named relay.edufeed.org causes Docker
DNS to hijack hostname resolution on the shared proxy network. Fix is
to rename the container in the homelab Ansible config.
2026-04-10 10:00:52 +02:00
.forgejo/workflows Fix Docker build by removing registry cache references 2026-04-09 21:25:44 +02:00
cmd/operator Add diagnostic logging for room caching, provisioning, and token requests 2026-04-09 21:40:32 +02:00
deploy Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
internal Remove diagnostic relay logging after identifying Docker DNS collision 2026-04-10 10:00:52 +02:00
.env.example Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
.env.prod.example Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
.gitignore Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
CLAUDE.md Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
docker-compose.prod.yml Add NIP-65 outbox-model relay discovery for community events 2026-04-09 16:56:38 +02:00
docker-compose.yml Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
Dockerfile Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
go.mod Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
go.sum Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00
README.md Initial commit: LiveKit Nostr Operator 2026-04-09 13:59:40 +02:00

LiveKit Nostr Operator

Go service that bridges Communikey communities with LiveKit for real-time video/audio conferencing. Community members authenticate via Nostr (NIP-98) and receive LiveKit JWTs to join rooms.

How It Works

The operator has two main functions:

  1. Relay Watcher — Subscribes to Nostr relays for kind 30312 (live activity) events. When a room event appears for an authorized community, it provisions a corresponding LiveKit room. When the event is deleted or expires, it destroys the room.

  2. Token EndpointPOST /token accepts a NIP-98 signed HTTP auth event. The operator verifies the signature, checks that the requester is a member of the community (via kind 30000 profile lists), and returns a LiveKit JWT + connection URL.

A background cleanup goroutine periodically removes stale rooms that no longer have active Nostr events.

Nostr Specs & Event Kinds

Spec Kind Purpose
NIP-98 (HTTP Auth) 27235 Client authenticates POST /token requests
NIP-53 pattern 30312 Live activity / room events watched by relay watcher
NIP-53 pattern 10312 Presence / participation events
NIP-40 (Expiration) Room events use expiration tag for auto-cleanup
Communikey 10222 Community definition (name, relays, livekit tag)
Communikey 30000 Profile lists (community membership)

Auth Flow

Client                         Operator                    LiveKit
  │                               │                           │
  │  POST /token                  │                           │
  │  Authorization: Nostr <base64 kind 27235 event>           │
  │  Body: { room, community }    │                           │
  │──────────────────────────────>│                           │
  │                               │  Verify NIP-98 signature  │
  │                               │  Check community membership│
  │                               │  Verify room exists & open │
  │                               │  Verify operator URL match │
  │                               │                           │
  │  { token: "<JWT>", url: "…" } │                           │
  │<──────────────────────────────│                           │
  │                               │                           │
  │  Connect with JWT             │                           │
  │──────────────────────────────────────────────────────────>│

Configuration

Variable Default Description
LIVEKIT_URL ws://localhost:7880 LiveKit server URL (internal/SDK use)
LIVEKIT_CLIENT_URL (falls back to LIVEKIT_URL) LiveKit URL returned to browser clients. In Docker, set to ws://localhost:7880
LIVEKIT_API_KEY LiveKit API key (must match server config)
LIVEKIT_API_SECRET LiveKit API secret (must match server config)
NOSTR_RELAYS Comma-separated relay URLs to watch for room events
AUTHORIZED_COMMUNITIES (empty) Comma-separated hex pubkeys of communities this operator serves. Empty = allow all (useful for local dev)
LISTEN_ADDR :8080 HTTP server listen address
OPERATOR_URL This operator's public URL (used to verify community livekit tag matches)
NIP98_TIME_WINDOW 60 NIP-98 auth timestamp tolerance in seconds

API Endpoints

Endpoint Method Auth Description
/token POST NIP-98 Verify community membership, return LiveKit JWT + connection URL
/health GET None Liveness probe — checks LiveKit connectivity (503 if unreachable)
/ready GET None Readiness probe — checks LiveKit + at least one community cached (503 if not ready)

Running

docker compose up

This starts a LiveKit dev server on port 7880 and the operator on port 8090.

Manual

# Set environment variables (see .env.example)
export LIVEKIT_URL=ws://localhost:7880
export LIVEKIT_API_KEY=devkey
export LIVEKIT_API_SECRET=secret
export NOSTR_RELAYS=wss://relay.example.com

go build -o operator ./cmd/operator
./operator

Production Deployment

Architecture

                  ┌──────────────────────────────────────┐
                  │              VPS / Server             │
Internet          │                                      │
─────────────────►│  Caddy (:443)                        │
  HTTPS/WSS       │    ├── /token, /health, /ready       │
                  │    │     → operator (:8080)           │
                  │    └── /livekit/*                     │
                  │          → livekit (:7880)            │
                  │                                      │
                  │  LiveKit (:7882/udp) ◄───────────────┼── WebRTC UDP
                  └──────────────────────────────────────┘

Caddy handles TLS termination and reverse-proxies to the operator and LiveKit. WebRTC media flows over UDP port 7882.

Quick Start

# 1. Copy and fill in environment
cp .env.prod.example .env
# Edit .env — set DOMAIN, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, etc.

# 2. Set your domain in TURN config
#    Edit deploy/livekit.yaml → turn.domain

# 3. Launch
docker compose -f docker-compose.prod.yml up -d

# 4. Verify
curl https://your-domain.com/health

Key Files

File Purpose
docker-compose.prod.yml Production stack: Caddy + LiveKit + operator
deploy/Caddyfile Reverse proxy routes, rate limiting (30 req/min on /token)
deploy/livekit.yaml LiveKit server config (ports, TURN, room limits)
.env.prod.example Template for production environment variables
Dockerfile Multi-stage Go build for the operator

TURN Configuration

TURN allows clients behind restrictive NATs/firewalls to connect. Edit deploy/livekit.yaml and set turn.domain to your server's domain. Caddy handles TLS termination (external_tls: true), so no separate TLS cert is needed for TURN.

Resource Sizing

Service CPU Memory Notes
Operator 0.25 vCPU 64 MB Lightweight — HTTP + Nostr relay subscriptions
LiveKit 2+ vCPU 12 GB Scales with participant count and media quality
Caddy 0.25 vCPU 64 MB TLS termination + reverse proxy

A 2 vCPU / 4 GB VPS is a reasonable minimum for small deployments.

Development

go test ./...        # Run all tests
go vet ./...         # Lint
go build ./cmd/...   # Build