- Go 99.5%
- Dockerfile 0.5%
|
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m9s
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. |
||
|---|---|---|
| .forgejo/workflows | ||
| cmd/operator | ||
| deploy | ||
| internal | ||
| .env.example | ||
| .env.prod.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.prod.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
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:
-
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.
-
Token Endpoint —
POST /tokenaccepts 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 (recommended for local dev)
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 | 1–2 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