Self-Hosting
Self-Hosting
Run the Realtime server on your own infrastructure with full control over data and compliance.
Docker
The easiest way to self-host:
docker run -d \
-p 8080:8080 \
-e JWT_SECRET=your-secret-key \
-e DATABASE_URL=postgres://user:pass@host:5432/realtime \
ghcr.io/actadv/realtime-server:latest
Docker Compose
For production with PostgreSQL and Redis:
# docker-compose.yml
version: '3.8'
services:
realtime:
image: ghcr.io/actadv/realtime-server:latest
ports:
- "8080:8080"
environment:
- JWT_SECRET=${JWT_SECRET}
- DATABASE_URL=postgres://postgres:postgres@db:5432/realtime
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:16-alpine
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=realtime
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SECRET | Yes | - | Secret for JWT verification |
DATABASE_URL | No | - | PostgreSQL connection string |
REDIS_URL | No | - | Redis for horizontal scaling |
PORT | No | 8080 | Server port |
HOST | No | 0.0.0.0 | Bind address |
JWT_ISSUER | No | - | Expected JWT issuer |
JWT_AUDIENCE | No | - | Expected JWT audience |
Webhook Runtime Configuration
The server can source webhook endpoint configuration from the database, environment, or both.
| Variable | Default | Description |
|---|---|---|
WEBHOOK_MODE | db | disabled, db, env, or hybrid |
WEBHOOK_ENV_CONFIG_JSON | - | JSON array of static webhook endpoints (used by env/hybrid) |
WEBHOOK_TIMEOUT_MS | 30000 | HTTP delivery timeout per attempt |
WEBHOOK_RETRY_MAX_ATTEMPTS | 5 | Maximum attempts including initial delivery |
WEBHOOK_RETRY_BASE_DELAY_MS | 1000 | Retry base backoff delay |
WEBHOOK_RETRY_MAX_DELAY_MS | 3600000 | Retry max backoff delay |
WEBHOOK_RETRY_JITTER_FACTOR | 0.2 | Retry jitter ratio |
Mode behavior:
disabled: webhook dispatcher is not created.db: webhook subscriptions are read from thewebhookstable.env: webhook subscriptions come only fromWEBHOOK_ENV_CONFIG_JSON.hybrid: subscriptions come from both DB and env configuration.
WEBHOOK_ENV_CONFIG_JSON example:
[
{
"customerId": "cust_123",
"url": "https://example.com/realtime-webhooks",
"secret": "whsec_live_123",
"events": ["room:join", "room:leave", "document:update"],
"enabled": true
}
]
Failure handling expectations:
- If dispatcher startup config is invalid in
envmode, webhook runtime is disabled (fail closed). - If env config is invalid in
hybridmode, the server logs an error and continues with DB-backed webhooks (fail open). - If webhook target lookup fails for an event, dispatch for that event is skipped and logged; room operations continue.
- If delivery log persistence fails, delivery attempts still proceed and failures are logged.
Database Setup
Run migrations to create the required tables:
# Using the Docker image
docker run --rm \
-e DATABASE_URL=postgres://... \
ghcr.io/actadv/realtime-server:latest \
pnpm db:migrate
Or create tables manually:
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id TEXT NOT NULL,
doc_id TEXT NOT NULL,
state BYTEA NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now(),
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE(room_id, doc_id)
);
CREATE INDEX idx_documents_room_doc ON documents(room_id, doc_id);
Horizontal Scaling
For multiple server instances, configure Redis for pub/sub:
REDIS_URL=redis://your-redis-host:6379
All instances will sync presence, cursors, and document updates through Redis.
AWS Deployment
See the CDK infrastructure for production AWS deployment including:
- ECS Fargate for the server
- Aurora Serverless v2 for PostgreSQL
- ElastiCache for Redis
- NLB with TLS termination
- Global Accelerator for anycast IPs
Health Checks
The server exposes a health endpoint:
GET /health
{
"status": "ok",
"connections": 42,
"rooms": 10,
"members": 85
}
Client Configuration
Point your client to your self-hosted server:
<RealtimeProvider
endpoint="wss://realtime.yourdomain.com"
authToken={getToken}
>
{children}
</RealtimeProvider>