All configuration is done through environment variables in the .env file.
| Variable | Required | Default | Description |
|---|---|---|---|
DOMAIN_BASE |
Yes | — | Base domain (e.g., example.com). Used to construct cp.example.com, relay.example.com |
ACME_EMAIL |
Yes | — | Email for Let's Encrypt certificate notifications |
JWT_SECRET |
Yes | — | Secret key for JWT signing. Generate with openssl rand -hex 32 |
| Variable | Required | Default | Description |
|---|---|---|---|
POSTGRES_USER |
No | relaycp |
PostgreSQL username |
POSTGRES_PASSWORD |
Yes | — | PostgreSQL password |
POSTGRES_DB |
No | relaycp |
PostgreSQL database name |
DATABASE_URL |
Auto | — | Full connection URL (auto-generated from above) |
| Variable | Required | Default | Description |
|---|---|---|---|
MINIO_ROOT_USER |
No | relay |
MinIO root username |
MINIO_ROOT_PASSWORD |
Yes | — | MinIO root password |
These create the first admin user when the database is empty.
| Variable | Required | Default | Description |
|---|---|---|---|
BOOTSTRAP_ADMIN_EMAIL |
Yes | — | Email for initial admin account |
BOOTSTRAP_ADMIN_PASSWORD |
Yes | — | Password for initial admin (min 8 chars) |
| Variable | Required | Default | Description |
|---|---|---|---|
SERVER_NAME |
No | EVC Team Relay |
Display name shown in plugin |
SERVER_ID |
No | (auto) | Unique identifier, defaults to relay key ID |
RELAY_PUBLIC_URL |
No | wss://relay.${DOMAIN_BASE} |
Public WebSocket URL for relay server |
| Variable | Required | Default | Description |
|---|---|---|---|
LOG_LEVEL |
No | INFO |
Log level: DEBUG, INFO, WARNING, ERROR |
LOG_FORMAT |
No | json |
Log format: json or text |
| Variable | Required | Default | Description |
|---|---|---|---|
EMAIL_ENABLED |
No | false |
Enable email sending |
SMTP_HOST |
If email | — | SMTP server hostname |
SMTP_PORT |
No | 587 |
SMTP port (587 for TLS, 465 for SSL) |
SMTP_USER |
If email | — | SMTP username |
SMTP_PASSWORD |
If email | — | SMTP password |
SMTP_USE_TLS |
No | true |
Use STARTTLS |
EMAIL_FROM |
If email | — | From address for emails |
EMAIL_REPLY_TO |
No | — | Reply-to address |
| Variable | Required | Default | Description |
|---|---|---|---|
WEB_PUBLISH_DOMAIN |
No | (disabled) | Domain for web publishing (e.g., docs.example.com) |
| Variable | Required | Default | Description |
|---|---|---|---|
GRAFANA_ADMIN_USER |
No | admin |
Grafana admin username |
GRAFANA_ADMIN_PASSWORD |
No | admin |
Grafana admin password |
| Variable | Required | Default | Description |
|---|---|---|---|
BACKUP_RETENTION |
No | 7 |
Days to keep local backups |
BACKUP_HOUR |
No | 2 |
Hour (UTC) to run daily backup |
BACKUP_RUN_ON_STARTUP |
No | true |
Run backup when container starts |
BACKUP_S3_ENABLED |
No | false |
Upload backups to S3 |
BACKUP_S3_BUCKET |
If S3 | — | S3 bucket name for backups |
| Variable | Required | Default | Description |
|---|---|---|---|
WEBHOOK_WORKER_INTERVAL |
No | 30 |
Webhook worker poll interval (seconds) |
WEBHOOK_WORKER_BATCH_SIZE |
No | 50 |
Max webhooks per cycle |
EMAIL_WORKER_INTERVAL |
No | 60 |
Email worker poll interval (seconds) |
EMAIL_WORKER_BATCH_SIZE |
No | 100 |
Max emails per cycle |
Configure in Admin Panel → Settings → OAuth, or via API:
curl -X POST https://cp.example.com/v1/admin/settings/oauth \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"provider": "generic",
"client_id": "your-client-id",
"client_secret": "your-client-secret",
"authorize_url": "https://auth.example.com/authorize",
"token_url": "https://auth.example.com/token",
"userinfo_url": "https://auth.example.com/userinfo"
}'Built-in rate limits (not configurable via env):
| Endpoint | Limit |
|---|---|
| Login | 10/minute |
| Share creation | 20/minute |
| Member operations | 30/minute |
| Token issuance | 30/minute |
DOMAIN_BASE=relay.example.com
ACME_EMAIL=admin@example.com
JWT_SECRET=your-very-long-random-secret-key-here
POSTGRES_PASSWORD=secure-postgres-password
MINIO_ROOT_PASSWORD=secure-minio-password
BOOTSTRAP_ADMIN_EMAIL=admin@example.com
BOOTSTRAP_ADMIN_PASSWORD=secure-admin-password# Domain
DOMAIN_BASE=example.com
ACME_EMAIL=ops@example.com
# Security
JWT_SECRET=64-character-random-hex-string
POSTGRES_PASSWORD=strong-random-password
MINIO_ROOT_PASSWORD=strong-random-password
# Admin
BOOTSTRAP_ADMIN_EMAIL=admin@example.com
BOOTSTRAP_ADMIN_PASSWORD=strong-admin-password
# Branding
SERVER_NAME=Acme Corp Relay
# Logging
LOG_LEVEL=INFO
LOG_FORMAT=json
# Email
EMAIL_ENABLED=true
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASSWORD=your-sendgrid-api-key
EMAIL_FROM=relay@example.com
# Web Publishing
WEB_PUBLISH_DOMAIN=docs.example.com
# Monitoring
GRAFANA_ADMIN_PASSWORD=secure-grafana-password
# Backups
BACKUP_RETENTION=30
BACKUP_S3_ENABLED=true
BACKUP_S3_BUCKET=relay-backupsThe relay server uses relay/relay.toml for configuration:
[store]
# S3-compatible storage (MinIO)
type = "s3"
bucket = "relay"
endpoint = "http://minio:9000"
region = "us-east-1"
access_key = "your-minio-user"
secret_key = "your-minio-password"
[auth]
# Control plane public key endpoint
public_key_url = "http://control-plane:8000/keys/public"The Caddyfile in the infra/ directory configures Caddy as a reverse proxy with automatic HTTPS. It also handles WebSocket token proxying for the relay server.
The relay server authenticates WebSocket connections using CWT (CBOR Web Token) tokens passed via the Authorization: Bearer header. However, the browser WebSocket API (new WebSocket(url)) does not support custom headers.
To bridge this gap, Caddy extracts the ?token= query parameter and sets it as the Authorization header:
relay.{$DOMAIN_BASE} {
@token_in_query query token=*
request_header @token_in_query Authorization "Bearer {query.token}"
reverse_proxy relay-server:8080
}How it works:
- The Obsidian plugin connects to
wss://relay.yourdomain.com/doc/ws/{docId}?token=<CWT> - Caddy sees the
?token=query parameter and setsAuthorization: Bearer <CWT>header - The relay server receives the
Authorizationheader and validates the CWT token
Both methods are supported simultaneously:
?token=query parameter — used by browser-based clients (Obsidian plugin)Authorization: Bearerheader — used by server-side clients and scripts
Relay tokens use CWT (CBOR Web Token) format with Ed25519 signatures instead of JWT. The token structure:
- Outer wrapper: CBOR Tag 61 (CWT)
- Inner wrapper: CBOR Tag 18 (COSE_Sign1)
- Claims:
iss(issuer),iat(issued at),scope(e.g.,doc:{id}:rw) - Signature: Ed25519
The control plane generates CWT tokens via POST /tokens/relay. The relay server verifies them using the Ed25519 public key configured in relay.toml.