> ███████╗██╗ ██╗███████╗███╗ ██╗████████╗███████╗
> ██╔════╝██║ ██║██╔════╝████╗ ██║╚══██╔══╝██╔════╝
> █████╗ ██║ ██║█████╗ ██╔██╗ ██║ ██║ ███████╗
> ██╔══╝ ╚██╗ ██╔╝██╔══╝ ██║╚██╗██║ ██║ ╚════██║
> ███████╗ ╚████╔╝ ███████╗██║ ╚████║ ██║ ███████║
> ╚══════╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝
prostaff-events — Real-time Event Bus & WebSocket Hub
╔══════════════════════════════════════════════════════════════════════════════╗
║ PROSTAFF EVENTS — Elixir / Phoenix 1.7 ║
╠══════════════════════════════════════════════════════════════════════════════╣
║ Real-time event bus for the ProStaff ecosystem. ║
║ Subscribes to Redis pub/sub from Rails and pushes events via WebSocket. ║
╚══════════════════════════════════════════════════════════════════════════════╝
▶ Key Features (click to expand)
┌─────────────────────────────────────────────────────────────────────────────┐
│ [■] Phoenix Channels — WebSocket delivery for all domain events │
│ [■] Redis Pub/Sub — PSUBSCRIBE prostaff:events:* from Rails API │
│ [■] InhouseQueue GenServer — One GenServer per active queue (BEAM actor) │
│ [■] Check-in Deadline — Process.send_after timer, no cron needed │
│ [■] Startup Reconciler — Rebuilds GenServers from Rails API on boot │
│ [■] JWT Auth — User JWT + Internal JWT (service-to-service) │
│ [■] Tenant Isolation — org_id validated on every channel subscription│
│ [■] Scraper Webhook — POST /events/notify via X-API-Key │
│ [■] Health Endpoint — GET /health for Coolify + Traefik probes │
│ [■] Supervised Tree — OTP supervisor restarts any crashed process │
└─────────────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ 01 · Quick Start │
│ 02 · Technology Stack │
│ 03 · Architecture │
│ 04 · Setup │
│ 05 · WebSocket Channels │
│ 06 · Domain Events │
│ 07 · Deployment │
│ 08 · Environment Variables │
└──────────────────────────────────────────────────────┘
▶ Docker (Recommended)
# Start the events service (requires Redis running separately or via prostaff-api stack)
cp .env.example .env
# Edit .env — set REDIS_URL, INTERNAL_JWT_SECRET, SECRET_KEY_BASE
docker compose up -d
# Health check
curl http://localhost:4000/health
# {"status":"ok"}▶ Local Development (Without Docker)
cp .env.example .env
# Edit .env
mix deps.get
mix phx.server
# Listening on http://localhost:4000 Events WS: ws://localhost:4000/socket
Health: http://localhost:4000/health
╔══════════════════════╦════════════════════════════════════════════════════╗
║ LAYER ║ TECHNOLOGY ║
╠══════════════════════╬════════════════════════════════════════════════════╣
║ Language ║ Elixir 1.17 ║
║ Framework ║ Phoenix 1.7 ║
║ Real-time ║ Phoenix Channels (WebSocket) ║
║ Pub/Sub ║ Phoenix.PubSub + Redix (Redis subscriber) ║
║ State ║ GenServer (one per active InhouseQueue) ║
║ Auth ║ Joken 2.6 (JWT verification) ║
║ Transport ║ Redis pub/sub (channel: prostaff:events:{org_id}) ║
║ HTTP server ║ Plug.Cowboy 2.7 ║
║ CORS ║ Corsica 2.1 ║
╚══════════════════════╩════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────────────┐
│ prostaff-events │
│ │
│ Rails API ──publish──▶ Redis pub/sub ──PSUBSCRIBE──▶ RedisSubscriber │
│ prostaff:events:* │ │
│ ┌─────────┘ │
│ ▼ │
│ Phoenix.PubSub │
│ / | \ │
│ ▼ ▼ ▼ │
│ Notif. Tourn. Inhouse InhouseQueue │
│ Channel Channel Channel GenServer │
│ │ │ │ (per queue) │
│ └────────┴───────┘ │
│ │ │
│ Connected Clients │
│ (Next.js / Discord bot) │
└─────────────────────────────────────────────────────────────────────────────┘
graph TD
S[ProstaffEvents.Supervisor]
S --> R[Registry — InhouseQueue.Registry]
S --> PS[Phoenix.PubSub]
S --> RS[RedisSubscriber — PSUBSCRIBE prostaff:events:*]
S --> DS[InhouseQueue.Supervisor — DynamicSupervisor]
S --> RC[InhouseQueue.Reconciler — fetches active queues on boot]
S --> EP[ProstaffEventsWeb.Endpoint]
DS --> GS1[InhouseQueue.Server org1:queue1]
DS --> GS2[InhouseQueue.Server org2:queue2]
style S fill:#4B275F
style RS fill:#d82c20
style DS fill:#4B275F
style GS1 fill:#FF6B35
style GS2 fill:#FF6B35
Rails publishes to Redis via EventPublishJob (Sidekiq, queue :events, retry: 0):
channel format: prostaff:events:{org_id}
envelope fields: id, type, user_id, org_id, payload, published_at
RedisSubscriber uses PSUBSCRIBE prostaff:events:* and routes by event type prefix
to the appropriate Phoenix.PubSub topic. Channels receive the broadcast and forward
to connected WebSocket clients.
[✓] Elixir 1.17+
[✓] Redis 7+ (shared with Rails API)
[✓] prostaff-api running (for InhouseQueue.Reconciler)
1. Clone and install dependencies:
git clone <repository-url>
cd prostaff-events
mix deps.get2. Configure environment:
cp .env.example .env
# Edit .env — see Section 08 for all variables3. Start the service:
mix phx.server4. Verify:
curl http://localhost:4000/health
# {"status":"ok","redis":"connected","version":"0.1.0"}import { Socket } from "phoenix"
const socket = new Socket("wss://events.prostaff.gg/socket", {
params: { token: "<user_jwt>" }
})
socket.connect()╔═══════════════════════════╦════════════════════╦═════════════════════════════╗
║ Topic ║ Subscriber ║ Auth requirement ║
╠═══════════════════════════╬════════════════════╬═════════════════════════════╣
║ notifications:{user_id} ║ Logged-in users ║ JWT — user_id must match ║
║ tournament:{id} ║ Any auth user ║ JWT ║
║ inhouse:{org_id} ║ Org members ║ JWT — org_id must match ║
╚═══════════════════════════╩════════════════════╩═════════════════════════════╝
// Notifications
const notifChannel = socket.channel(`notifications:${userId}`)
notifChannel.join()
notifChannel.on("notification.created", ({ notification }) => {
console.log(notification.title)
})
// Inhouse queue
const inhouseChannel = socket.channel(`inhouse:${orgId}`)
inhouseChannel.join()
inhouseChannel.on("queue_updated", ({ queue }) => { ... })
inhouseChannel.on("check_in_expired", ({ queue }) => { ... })
// Tournament
const tournamentChannel = socket.channel(`tournament:${tournamentId}`)
tournamentChannel.join()
tournamentChannel.on("match_confirmed", ({ match }) => { ... })Events published by the Rails API and delivered to connected clients:
┌────────────────────────────────┬───────────────────────────────────────────┐
│ Event type │ Publisher (Rails) │
├────────────────────────────────┼───────────────────────────────────────────┤
│ inhouse.session_started │ InhouseQueuesController#start_session │
│ scrim_request.accepted │ ScrimRequestsController#accept │
│ scrim_request.declined │ ScrimRequestsController#decline │
│ tournament_match.confirmed │ MatchConfirmationService#confirm_match! │
│ tournament_match.walkover │ TournamentWalkoverJob │
│ team_goal.completed │ TeamGoal#mark_as_completed! │
│ team_goal.progress_updated │ TeamGoal#update_progress! │
│ player.transferred │ Admin::PlayersController#transfer │
│ roster.player_removed │ RosterManagementService │
│ roster.player_hired │ RosterManagementService │
└────────────────────────────────┴───────────────────────────────────────────┘
The Python Scraper can push events directly without going through Redis:
POST /events/notify
X-Api-Key: <SCRAPER_API_KEY>
Content-Type: application/json
{ "type": "match.scraped", "org_id": "123", "payload": { ... } }graph TB
subgraph "Clients"
FE["Next.js frontend"]
Bot["Discord bot"]
end
subgraph "Production — Coolify"
Traefik["Traefik\nTLS + Let's Encrypt\nevents.prostaff.gg"]
end
subgraph "prostaff-events"
EP["Phoenix Endpoint\nport 4000"]
CH["Phoenix Channels\n(WS)"]
RS["RedisSubscriber"]
end
subgraph "prostaff-api"
Rails["Rails API"]
SQ["Sidekiq\n(EventPublishJob)"]
end
RD[("Redis\ncoolify network")]
FE -- "WSS" --> Traefik
Bot -- "WSS" --> Traefik
Traefik --> EP
EP --> CH
Rails --> SQ
SQ -- "PUBLISH prostaff:events:*" --> RD
RS -- "PSUBSCRIBE" --> RD
RS --> CH
style FE fill:#1e88e5
style Traefik fill:#1565c0
style RD fill:#d82c20
style Rails fill:#CC342D
Key points:
- Both services share the same Redis instance via the
coolifyDocker network - prostaff-events joins
coolify: external: true— no separate Redis container - Traefik handles TLS for
events.prostaff.ggvia Let's Encrypt - Internal communication between services uses container names (e.g.
http://api:3000)
For prostaff-events app:
REDIS_PASSWORD=<same password already set in prostaff-api>
INTERNAL_JWT_SECRET=<same value as prostaff-api>
SECRET_KEY_BASE=<64+ char random string — mix phx.gen.secret>
# RAILS_API_URL and PHX_HOST have sane defaults, override if neededFor prostaff-api app (add these two):
PHOENIX_EVENTS_ENABLED=true
PHOENIX_EVENTS_URL=http://events:4000The container name
eventsmatches the service name indocker-compose.yml. Verify in the Coolify dashboard if Coolify overrides it.
╔════════════════════════╦══════════╦══════════════════════════════════════════╗
║ Variable ║ Required║ Description ║
╠════════════════════════╬══════════╬══════════════════════════════════════════╣
║ REDIS_PASSWORD ║ yes ║ Redis password (same as prostaff-api) ║
║ INTERNAL_JWT_SECRET ║ yes ║ Must match prostaff-api value ║
║ SECRET_KEY_BASE ║ yes ║ Phoenix secret (min 64 chars) ║
║ RAILS_API_URL ║ no ║ Internal Rails URL (default: api:3000) ║
║ PHX_HOST ║ no ║ Public hostname (default: events.p.gg) ║
║ PORT ║ no ║ HTTP port (default: 4000) ║
║ SCRAPER_API_KEY ║ no ║ API key for POST /events/notify ║
╚════════════════════════╩══════════╩══════════════════════════════════════════╝
Generate a SECRET_KEY_BASE:
mix phx.gen.secret╔══════════════════════════════════════════════════════════════════════════════╗
║ © 2026 ProStaff.gg. All rights reserved. ║
║ ║
║ GNU Affero General Public License v3.0 (AGPLv3) ║
╚══════════════════════════════════════════════════════════════════════════════╝
Prostaff.gg isn't endorsed by Riot Games and doesn't reflect the views or opinions of Riot Games or anyone officially involved in producing or managing Riot Games properties.
Riot Games, and all associated properties are trademarks or registered trademarks of Riot Games, Inc.
▓▒░ · © 2026 PROSTAFF.GG · ░▒▓