Skip to content

Repository files navigation

BlitzChat

Real-time chat, powered by the BEAM.

Phoenix 1.8 · LiveView · Per-room GenServers · REST API · Production-ready


Elixir Phoenix LiveView PostgreSQL Docker Prometheus Sentry OpenAPI License: MIT


✨ Features

  • ⚡ Instant delivery — LiveView + PubSub push messages in under 100ms
  • 👥 Live presence & typing — Who's online, who's typing, updated in real time
  • 🧠 Per-room OTP — Each chat room is an isolated, supervised GenServer. Crash-safe, idle-shutdown after 5m
  • 🔑 Password auth — bcrypt-hashed passwords, register + login, session renewal
  • 🚪 Room membership — open-join with membership checks, max_members, archived-room lockout
  • 🔐 REST API — OpenAPI 3.0 spec, Swagger UI (dev), Bearer-token auth, scope-enforced endpoints
  • 🛡 Rate limiting — Per-user, per-API-key, per-IP buckets (Hammer + ETS)
  • 📊 Full observability — Token-protected Prometheus /metrics, Sentry, structured JSON logs, admin LiveDashboard
  • 🐳 Deploy anywhere — Multi-stage Dockerfile + prod docker-compose with Caddy (auto-TLS)
  • 🚑 Health probes/health (liveness) and /ready (DB + supervisor) for Kubernetes or compose
  • 🧪 Tested — Contexts, GenServers, plugs, REST, sessions, and LiveView authorization

🧭 Architecture

┌─────────────┐     ┌───────────────────────────────────┐     ┌──────────┐
│   Browser   │◄─WS─┤  Phoenix Endpoint (Bandit)        │──►──┤ Postgres │
└─────────────┘     │                                   │     └──────────┘
                    │  Router → Pipelines               │
 REST (Bearer) ────►│   ├─ :browser  (CSP, CSRF, HSTS)  │     ┌──────────┐
                    │   ├─ :api_auth (read scope)       │─────┤  Caddy   │
                    │   └─ :api_auth_write (write)      │     │  (TLS)   │
                    │                                   │     └──────────┘
                    │  RoomSupervisor (DynamicSupv.)    │
                    │   ├─ RoomServer room:a  (GenSrv)  │
                    │   ├─ RoomServer room:b  (GenSrv)  │
                    │   └─ ...                          │
                    │                                   │
                    │  TaskSupervisor   PubSub          │
                    │  Presence         Telemetry       │
                    └───────────────────────────────────┘

Each room spawns a named GenServer on first use, persists every message synchronously (no in-memory buffer, no data loss on crash), broadcasts via Phoenix.PubSub, and idle-shuts-down after 5 minutes of silence. Rooms are open-join: the first visit creates a membership; max_members and archived rooms are enforced.

🚀 Quick start

Local development

git clone https://github.com/pradhankukiran/blitz-chat.git
cd blitz-chat

# Start Postgres
docker compose up -d db

# Install deps, create DB, run migrations, seed
mix setup

# Start the server (IEx for hot reload)
iex -S mix phx.server

Open http://localhost:4000. Seeded local demo accounts (never use in production):

Username Password Role
alice password123 admin
bob password123 user
charlie password123 user

Register a new account at /register. Swagger UI at http://localhost:4000/swaggerui.

Production (Docker + Caddy auto-TLS)

cp .env.example .env
# Required: POSTGRES_PASSWORD, SECRET_KEY_BASE (mix phx.gen.secret),
#           PHX_HOST, METRICS_TOKEN (openssl rand -hex 32)

docker compose -f docker-compose.prod.yml up -d --build

# Always run migrations after deploy / image upgrade
docker compose -f docker-compose.prod.yml exec app /app/bin/migrate

Caddy issues Let's Encrypt certificates automatically on first boot (~30s).

Deploy notes

  • Migrations: the app container does not auto-migrate. Run /app/bin/migrate after every deploy.

  • Resource limits: prod compose sets memory/CPU caps on app, db, and caddy as a baseline; tune for your host.

  • Database TLS: set DATABASE_SSL=true (default in runtime) and optionally DATABASE_SSL_CA=/path/to/ca.pem for peer verification. Compose on a private network can use DATABASE_SSL=false.

  • Backups: Postgres data lives in the db_data volume. Example snapshot:

    docker compose -f docker-compose.prod.yml exec db \
      pg_dump -U blitz_chat blitz_chat_prod > backup-$(date +%F).sql
  • Metrics scrape: Prometheus must send Authorization: Bearer $METRICS_TOKEN or header x-metrics-token: $METRICS_TOKEN.

  • Single-node: room GenServers and ETS rate limits are process-local. Run one app instance (or sticky sessions + distributed process strategy) until you add multi-node room placement.

📡 REST API

All endpoints versioned under /api/v1 and require Authorization: Bearer <api-key>.

Message list/create and room stats require the API key owner to be able to access the room (open-join membership).

Method Path Scope Description
GET /rooms :read List rooms (paginated, max 100)
GET /rooms/:id :read Show a room
POST /rooms :write Create a room (creator auto-joins)
GET /rooms/:room_id/messages :read List messages in a room
POST /rooms/:room_id/messages :write Send a message (author = key owner)
GET /rooms/:room_id/stats :read Live per-process stats

Errors always use the same envelope:

{ "error": { "code": "validation_failed", "message": "Invalid input", "details": { "body": ["can't be blank"] } } }

🧩 Project structure

lib/
├─ blitz_chat/
│  ├─ accounts/            User schema + password auth context
│  ├─ api_keys/            API key schema + context (scopes, expiry, usage)
│  ├─ chat/                Room, Message, Membership schemas
│  │  ├─ room_server.ex    Per-room GenServer (persist-then-broadcast)
│  │  └─ room_supervisor   DynamicSupervisor with race-safe start
│  └─ release.ex           Release migration tasks (bin/migrate)
└─ blitz_chat_web/
   ├─ controllers/
   │  ├─ api/              REST (versioned /api/v1, FallbackController)
   │  ├─ health_controller  /health + /ready
   │  └─ metrics_controller /metrics (Prometheus)
   ├─ live/                LobbyLive · RoomLive · AdminDashboardLive
   ├─ plugs/               ApiKeyAuth · MetricsAuth · RateLimit · SecurityHeaders · SetCurrentUser
   └─ router.ex

config/                    config · dev · prod · runtime · test
priv/repo/migrations/      migrations (schemas, FKs, indexes, passwords, api keys)
rel/overlays/bin/          server · migrate (mix release)
test/                      ExUnit suite

Dockerfile   docker-compose.prod.yml   Caddyfile   .env.example

🧪 Testing

mix test             # full suite
mix precommit        # compile --warnings-as-errors + deps.unlock --unused + format + test

Coverage includes password auth, membership / capacity / archived rules, GenServer concurrency (50-way race test, concurrent sends, idle shutdown), plugs (rate limit, API key auth, metrics token, scope enforcement), REST controllers, and LiveView authorization gates.

Light load check (single room)

With the server running and a valid API key:

# Rough single-room write throughput check (adjust URL/key/room).
# Expect serial RoomServer + DB latency to bound results on one node.
for i in $(seq 1 50); do
  curl -s -o /dev/null -w "%{http_code}\n" \
    -H "Authorization: Bearer $API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"body\":\"load $i\"}" \
    "http://localhost:4000/api/v1/rooms/$ROOM_ID/messages" &
done
wait

This is a smoke check, not a formal benchmark. Message sends are serialized per room process.

🔐 Security

Layer Hardening
Browser auth bcrypt passwords, register + login, session renewal on sign-in/out
Session cookie Signed cookie; Secure flag in production
Headers CSP, HSTS, Permissions-Policy, Referrer-Policy on every HTML response
CSRF Enforced on all mutating browser routes
Transport WebSocket-only (longpoll disabled); check_origin pinned to PHX_HOST
Rooms Membership required to send; open-join; max_members; archived blocked
API auth Bearer tokens with :read / :write / :admin scopes
API keys SHA-256 hashed, prefix-searchable, expiry + revocation + usage tracking
Rate limits Login/register (10/min/IP), API writes (10–60/min/key), LiveView events
Mass-assignment FK fields never cast from user input
Admin access /admin and LiveDashboard role-gated
Metrics /metrics requires METRICS_TOKEN in production
Request size Plug.Parsers body cap 1 MB; message body max 5 KB
Secrets Fail-fast on missing SECRET_KEY_BASE, PHX_HOST, DATABASE_URL, METRICS_TOKEN

📊 Observability

Endpoint Purpose
/health Liveness (200 if process alive)
/ready Readiness (DB SELECT 1 + RoomSupervisor alive, 503 if not)
/metrics Prometheus scrape (token-protected in prod); no high-cardinality room_id labels
/admin/metrics Phoenix LiveDashboard (admin only)

Errors forward to Sentry when SENTRY_DSN is set at runtime. Logs are structured JSON (LoggerJSON.Formatters.Basic) in prod, plain text in dev.

🛣 Roadmap

  • Message edit / delete
  • File and image uploads
  • Markdown rendering with sanitization
  • Email / OAuth authentication
  • Redis backend for rate limiting (horizontal scale)
  • Multi-node room process distribution
  • End-to-end encryption
  • GitHub Actions CI pipeline

📄 License

MIT © Kiran Pradhan

About

Production-ready real-time chat in Elixir — Phoenix 1.8 LiveView, per-room GenServers, REST API with OpenAPI, rate limiting, Prometheus metrics, Sentry, Docker + Caddy deploy.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages