Real-time video conferencing and chat platform built with Go, React, and WebRTC.
Chatter enables peer-to-peer video calls, audio communication, and text messaging through a modern web interface. It features JWT-based authentication, device-aware session management, WebSocket signaling, and a full observability stack.
- Features
- Architecture
- Tech Stack
- Project Structure
- Getting Started
- Configuration
- API Reference
- WebSocket Protocol
- Database Schema
- Observability
- Production Deployment
- Task Runner Commands
- Video Calls — peer-to-peer video via WebRTC with STUN/TURN support
- Audio Calls — real-time audio streaming between participants
- Text Chat — instant messaging within rooms
- Room System — create and join rooms by ID or direct WebSocket URL
- Presence — real-time join/leave notifications and participant list
- Display Names — custom names visible to all room participants
- User Registration & Login — secure username/password authentication
- JWT Access Tokens — short-lived tokens for API authorization
- Refresh Tokens — long-lived tokens stored as SHA-256 hashes in the database
- Device-aware Sessions — each device tracked independently via
X-Device-ID - Session Management — view all active sessions, identify current device
- Auto Token Refresh — seamless token renewal without re-login
- Containerized Deployment — full Docker Compose setup for dev and production
- Reverse Proxy — Nginx with SSL/TLS termination
- SSL Certificates — automated Let's Encrypt via Certbot
- Database Migrations — versioned schema changes with Goose
- Health Checks — all services monitored with Docker health checks
- Metrics — Prometheus metrics for HTTP requests, duration, and response size
- Distributed Tracing — OpenTelemetry traces exported to Tempo
- Log Aggregation — structured JSON logs shipped to Loki via Docker logging driver
- Dashboards — Grafana with pre-configured Prometheus, Loki, and Tempo datasources
graph TB
subgraph Client["Browser"]
FE["React SPA<br/>(Vite)"]
WR["WebRTC Engine"]
end
subgraph Backend["Go Backend"]
API["REST API<br/>(net/http)"]
WS["WebSocket<br/>Signaling Server"]
AUTH["Auth Service"]
JWT["JWT Manager"]
REPO["Repositories"]
end
subgraph Data["Data Layer"]
PG[(PostgreSQL)]
RD[(Redis<br/><i>reserved</i>)]
end
subgraph Monitoring["Observability"]
PROM["Prometheus"]
TEMPO["Tempo"]
LOKI["Loki"]
GRAF["Grafana"]
end
FE -->|HTTP| API
FE -->|WebSocket| WS
WR <-->|P2P Media| WR
API --> AUTH
AUTH --> JWT
AUTH --> REPO
REPO --> PG
API -->|/metrics| PROM
API -.->|traces| TEMPO
API -.->|logs| LOKI
PROM --> GRAF
TEMPO --> GRAF
LOKI --> GRAF
sequenceDiagram
participant C as Client
participant S as Server
participant DB as PostgreSQL
C->>S: POST /auth/register {username, password}
Note over S: Validate credentials
S->>S: Hash password (bcrypt)
S->>DB: INSERT INTO users
S->>S: Generate access token (JWT)
S->>S: Generate refresh token (random)
S->>DB: INSERT INTO refresh_tokens (hash)
S-->>C: {id, token, username} + Set-Cookie: refresh_token
sequenceDiagram
participant A as Peer A
participant S as Signaling Server
participant B as Peer B
A->>S: WebSocket connect /ws/{roomId}
S-->>A: welcome {clientId}
B->>S: WebSocket connect /ws/{roomId}
S-->>B: welcome {clientId}
S-->>A: presence {join, B}
S-->>B: participants [A, B]
Note over A,B: WebRTC negotiation via server relay
A->>S: webrtc {offer, to: B, sdp}
S-->>B: webrtc {offer, from: A, sdp}
B->>S: webrtc {answer, to: A, sdp}
S-->>A: webrtc {answer, from: B, sdp}
A->>S: webrtc {ice, to: B, candidate}
S-->>B: webrtc {ice, from: A, candidate}
Note over A,B: Direct P2P media stream established
graph LR
subgraph Handler["Handler Layer"]
H1["auth.go"]
H2["signaling/handler.go"]
end
subgraph UseCase["Use Case Layer"]
U1["auth.go"]
end
subgraph Repository["Repository Layer"]
R1["auth.go"]
R2["refresh_token.go"]
end
subgraph Domain["Domain Layer"]
D1["user.go"]
D2["token.go"]
end
subgraph Infra["Infrastructure"]
I1["jwt_manager.go"]
I2["postgres.go"]
end
H1 --> U1
H2 --> U1
U1 --> R1
U1 --> R2
U1 --> I1
R1 --> D1
R2 --> D2
R1 --> I2
R2 --> I2
| Layer | Technology | Purpose |
|---|---|---|
| Backend | Go >1.25 | API server, signaling, business logic |
| Frontend | React 18 + Vite | Single-page application |
| Real-time | WebSocket + WebRTC | Signaling and peer-to-peer media |
| Database | PostgreSQL 18 | Users, refresh tokens |
| Cache | Redis 7.4 | Reserved for future use |
| Auth | JWT (HS256) + bcrypt | Access/refresh token system |
| Migrations | Goose | Versioned database schema |
| Metrics | Prometheus | HTTP metrics collection |
| Tracing | OpenTelemetry + Tempo | Distributed request tracing |
| Logs | Zap + Loki | Structured JSON logging |
| Dashboards | Grafana | Unified observability UI |
| Proxy | Nginx | Reverse proxy, SSL termination |
| SSL | Certbot (Let's Encrypt) | Automated certificate management |
| Containers | Docker + Docker Compose | Dev and production orchestration |
| Task Runner | Taskfile | Development commands |
chatter/
├── chatter/ # Go backend
│ ├── cmd/
│ │ └── main.go # Entry point
│ ├── config/
│ │ └── config.yaml # Application config
│ ├── internal/
│ │ ├── app/
│ │ │ └── app.go # HTTP server setup & routing
│ │ ├── config/
│ │ │ └── config.go # Config struct & loader
│ │ ├── domain/
│ │ │ ├── user.go # User entity
│ │ │ └── token.go # RefreshToken entity
│ │ ├── handler/
│ │ │ └── auth.go # Auth HTTP handlers
│ │ ├── infra/
│ │ │ └── jwt_manager.go # JWT generation & validation
│ │ ├── repository/
│ │ │ ├── auth.go # User repository (PostgreSQL)
│ │ │ └── refresh_token.go # Token repository (PostgreSQL)
│ │ ├── signaling/
│ │ │ ├── client.go # WebSocket client
│ │ │ ├── handler.go # Room creation & WS handler
│ │ │ ├── registry.go # Thread-safe room registry
│ │ │ └── room.go # Room logic & broadcasting
│ │ └── usecase/
│ │ └── auth.go # Auth business logic
│ ├── migrations/
│ │ ├── 00001_create_users_table.sql
│ │ ├── 00002_create_refresh_tokens_table.sql
│ │ └── 00003_add_device_id.sql
│ ├── pkg/
│ │ ├── logger/ # Zap logger setup
│ │ ├── middleware/
│ │ │ ├── auth.go # JWT auth middleware
│ │ │ ├── cors.go # CORS middleware
│ │ │ ├── logging.go # Request logging
│ │ │ ├── metrics.go # Prometheus metrics
│ │ │ ├── response_recorder.go # ResponseWriter wrapper
│ │ │ └── tracing.go # OpenTelemetry tracing
│ │ ├── postgres/ # PostgreSQL connection pool
│ │ ├── redis/ # Redis client (reserved)
│ │ └── telemetry/ # OpenTelemetry exporter
│ ├── Dockerfile
│ ├── go.mod
│ └── go.sum
├── web/ # React frontend
│ ├── src/
│ │ ├── App.jsx # All pages & WebRTC logic
│ │ ├── main.jsx # React entry point
│ │ └── styles.css # Application styles
│ ├── Dockerfile
│ ├── nginx.conf # SPA routing
│ ├── vite.config.js
│ ├── package.json
│ └── index.html
├── infra/
│ ├── monitoring/
│ │ ├── grafana/provisioning/ # Grafana datasources
│ │ ├── loki/ # Loki retention config
│ │ ├── prometheus/ # Prometheus scrape config
│ │ └── tempo/ # Tempo storage config
│ └── nginx/
│ ├── init.conf # Bootstrap nginx (pre-SSL)
│ └── prod.conf # Production nginx with SSL
├── docker-compose.yaml # Development environment
├── docker-compose.prod.yaml # Production environment
├── Dockerfile.migrate # Goose migration runner
├── Taskfile.yml # Task runner commands
└── init-letsencrypt.sh # SSL certificate bootstrap
1. Clone the repository
git clone https://github.com/your-username/chatter.git
cd chatter2. Start the development environment
task dev:upOr without Task:
docker compose up -d --build3. Run database migrations
task migrate:upOr without Task:
docker compose run --rm migrate up4. Open the application
| Service | URL |
|---|---|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:8080 |
| Grafana | http://localhost:3000 |
| Prometheus | http://localhost:9090 |
task dev:downConfiguration is loaded from chatter/config/config.yaml and can be overridden with environment variables.
server:
addr: ":8080"
cors_origins:
- "http://localhost:5173"
auth:
secret: "a-string-secret-at-least-256-bits-long"
access_ttl: 24h
refresh_ttl: 1440h
postgres:
host: chatter-postgres
port: 5432
username: postgres
password: password
database: postgres
min_conns: 1
max_conns: 100
redis:
host: redis
port: 6379
username: default
password: password
db: 0| Variable | Description | Default |
|---|---|---|
CONFIG_PATH |
Path to config file | config/config.yaml |
SERVER_ADDR |
Server listen address | :8080 |
SERVER_CORS_ORIGINS |
Comma-separated allowed origins | http://localhost:5173 |
AUTH_SECRET |
JWT signing secret (min 256 bits) | — |
AUTH_ACCESS_TTL |
Access token lifetime | 24h |
AUTH_REFRESH_TTL |
Refresh token lifetime | 1440h |
POSTGRES_HOST |
PostgreSQL host | chatter-postgres |
POSTGRES_PORT |
PostgreSQL port | 5432 |
POSTGRES_USER |
PostgreSQL username | postgres |
POSTGRES_PASSWORD |
PostgreSQL password | — |
POSTGRES_DB |
PostgreSQL database name | postgres |
OTEL_SERVICE_NAME |
OpenTelemetry service name | chatter |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP exporter endpoint | http://tempo:4318 |
Base URL: http://localhost:8080
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check — returns 200 OK |
GET |
/metrics |
Prometheus metrics endpoint |
POST /auth/register
Headers:
| Header | Required | Description |
|---|---|---|
Content-Type |
Yes | application/json |
X-Device-ID |
Yes | Unique device identifier |
Body:
{
"username": "alice",
"password": "securepassword"
}Response 200:
{
"id": 1,
"token": "eyJhbGciOiJIUzI1NiIs...",
"username": "alice"
}Sets refresh_token HttpOnly cookie.
POST /auth/login
Headers: same as Register
Body:
{
"username": "alice",
"password": "securepassword"
}Response 200:
{
"id": 1,
"token": "eyJhbGciOiJIUzI1NiIs...",
"username": "alice"
}POST /auth/refresh
Cookies: refresh_token (set during login/register)
Response 200:
{
"id": 1,
"token": "eyJhbGciOiJIUzI1NiIs...",
"username": "alice"
}POST /auth/logout
Clears the refresh_token cookie.
GET /auth/sessions
Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <access_token> |
Response 200:
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"deviceId": "abc123def456",
"expiresAt": "2026-04-07T12:00:00Z",
"lastSeen": "2026-02-07T12:00:00Z"
}
]POST /rooms
Headers:
| Header | Required | Description |
|---|---|---|
Authorization |
Yes | Bearer <access_token> |
Response 200:
{
"roomId": "a1b2c3d4",
"wsUrl": "ws://localhost:8080/ws/a1b2c3d4"
}GET /ws/{roomId}?token=<access_token>
Upgrades to WebSocket connection. The token query parameter is optional but enables authenticated participation.
All messages are JSON. The type field determines the message kind.
| Type | Description | Payload |
|---|---|---|
welcome |
Sent on connection | {clientId} |
participants |
Current room members | {participants: [{id, displayName}]} |
presence |
Join/leave events | {action: "join"|"leave", clientId} |
profile |
Display name update | {clientId, displayName} |
| Type | Description | Payload |
|---|---|---|
profile |
Set display name | {type: "profile", displayName} |
chat |
Send text message | {type: "chat", text} |
webrtc |
Signaling message | {type: "webrtc", action, to, from, sdp|candidate} |
| Action | Direction | Description |
|---|---|---|
offer |
A → B | SDP offer for peer connection |
answer |
B → A | SDP answer accepting the offer |
ice |
Both | ICE candidate exchange for connectivity |
erDiagram
USERS {
bigserial id PK
text username UK
text email
bytea password_hash
timestamptz created_at
timestamptz updated_at
}
REFRESH_TOKENS {
uuid id PK
integer user_id FK
text token_hash
timestamp expires_at
boolean revoked
timestamptz updated_at
varchar device_id
}
USERS ||--o{ REFRESH_TOKENS : "has many"
| Version | Description |
|---|---|
00001 |
Create users table with username index |
00002 |
Create refresh_tokens table with foreign key to users |
00003 |
Add device_id column to refresh tokens |
graph LR
APP["Chatter<br/>Backend"] -->|scrape /metrics| PROM["Prometheus<br/>:9090"]
APP -->|OTLP HTTP| TEMPO["Tempo<br/>:4318"]
APP -->|Docker logs| LOKI["Loki<br/>:3100"]
PROM --> GRAF["Grafana<br/>:3000"]
TEMPO --> GRAF
LOKI --> GRAF
The backend exposes metrics at GET /metrics:
| Metric | Type | Labels | Description |
|---|---|---|---|
http_requests_total |
Counter | method, path, status | Total HTTP requests |
http_request_duration_ms_total |
Counter | method, path, status | Cumulative request duration |
http_response_size_bytes_total |
Counter | method, path, status | Cumulative response size |
Every HTTP request generates a span with attributes:
http.method— request methodhttp.route— matched route patternhttp.target— full request URLhttp.status_code— response status
Traces are exported via OTLP HTTP to Tempo at http://tempo:4318.
Structured JSON logs with fields:
method,path,status,bytes,durationremote_addr,userID,username- Health and metrics endpoints are excluded from logging
Logs are collected by Docker's Loki logging driver and shipped to Loki automatically.
Pre-configured datasources available out of the box:
| Datasource | URL | Purpose |
|---|---|---|
| Prometheus | http://prometheus:9090 |
Metrics queries |
| Loki | http://loki:3100 |
Log queries |
| Tempo | http://tempo:3200 |
Trace queries |
Access Grafana at http://localhost:3000 (default credentials: admin / admin).
Edit docker-compose.prod.yaml and set:
POSTGRES_PASSWORD— strong database passwordSERVER_CORS_ORIGINS— your domain (e.g.,https://example.com)AUTH_SECRET— strong JWT signing secret
Update domains in init-letsencrypt.sh, then run:
chmod +x init-letsencrypt.sh
sudo ./init-letsencrypt.shdocker compose -f docker-compose.prod.yaml up -d --buildgraph TB
INET["Internet"] --> NGINX["Nginx<br/>:80 / :443"]
NGINX -->|/api/*| BACKEND["Chatter<br/>:8080"]
NGINX -->|/ws/*| BACKEND
NGINX -->|/*| FRONTEND["React SPA<br/>(static)"]
BACKEND --> PG[(PostgreSQL)]
CERT["Certbot"] -.->|renew| NGINX
Services in production:
- Nginx — reverse proxy with SSL termination
- Certbot — automatic certificate renewal
- Web — React SPA served by Nginx
- Chatter — Go backend API + WebSocket server
- PostgreSQL — primary database
- Migrate — runs database migrations on startup
| Command | Description |
|---|---|
task dev:up |
Start development environment |
task dev:down |
Stop and remove all containers and volumes |
task migrate:up |
Run all pending migrations |
task migrate:down |
Rollback the last migration |
task migrate:status |
Show current migration status |
Every HTTP request passes through the following middleware chain:
graph LR
REQ["Request"] --> CORS["CORS"]
CORS --> TRACE["Tracing"]
TRACE --> LOG["Logging"]
LOG --> METRIC["Metrics"]
METRIC --> AUTH["Auth<br/><i>(protected routes)</i>"]
AUTH --> HANDLER["Handler"]
HANDLER --> RES["Response"]
| Middleware | Scope | Description |
|---|---|---|
| CORS | All routes | Handles preflight and sets CORS headers |
| Tracing | All routes | Creates OpenTelemetry span per request |
| Logging | All routes* | Logs request details with Zap (*excludes /health, /metrics) |
| Metrics | All routes | Records Prometheus counters per request |
| Auth | Protected routes | Validates JWT and injects user context |
Built with Go, React, and WebRTC