Skip to content

ishan739/PvPEngine

Repository files navigation

PvPEngine

"Stripe for Competitive Gaming" — A production-grade, multi-tenant B2B SaaS backend platform for competitive games.

Game studios integrate via REST APIs and webhooks instead of building their own matchmaking, rating, and leaderboard systems from scratch. PvPEngine handles the competitive infrastructure. The actual gameplay runs on the game studio's own servers.


Tech Stack

Layer Technology
Language Java 21
Framework Spring Boot 4.x
Primary DB PostgreSQL
Cache / Queues Redis
Event Streaming Apache Kafka
Schema Migrations Flyway
Containerization Docker
API Docs Swagger / OpenAPI

What PvPEngine Does

For Game Studios

  • Onboard your game and get API keys in minutes
  • No need to build matchmaking, rating, or leaderboard systems in-house
  • Every request is authenticated and tenant-isolated by API key
  • Receive real-time match notifications via HMAC-signed webhooks
  • Submit match results — PvPEngine handles ELO and leaderboard updates

For Players (via Game Studio)

  • Skill-based matchmaking — paired by rating proximity (SBMM)
  • ELO rating that updates automatically after every match, including draws
  • Real-time leaderboard per game, served from Redis

Architecture Overview

Multi-Tenancy Design

Every game studio is a separate tenant. Isolation is enforced at the API key level — not the request body. The API key is split into a prefix (stored plaintext for fast DB lookup) and a secret (BCrypt hashed, never stored raw). Every DB query is scoped by gameId resolved from the key — cross-tenant data access is structurally impossible.

Game Backend ──► X-API-Key Header
                      │
                 [Filter Layer]
                      │
              Split: prefix + secret
                      │
              DB lookup by prefix (O(1))
                      │
              BCrypt verify secret
                      │
              Set gameId → TenantContext (ThreadLocal)
                      │
              All queries scoped by gameId

Data Layer Responsibilities

Store Role
PostgreSQL Source of truth — games, players, matches, ratings, results, webhook audit
Redis Matchmaking queues (sorted sets by rating), leaderboard (sorted sets by rating)
Kafka Async domain event transport — match.completed.v1 for rating + leaderboard updates

Complete Match Lifecycle

1. Studio Onboarding     →  POST /api/v1/games + generate API key
2. Player Registration   →  POST /api/v1/players (isolated per tenant)
3. Matchmaking           →  Player joins Redis sorted set queue
                             Background worker pairs nearest-rated players every 3s
4. Match Found           →  Match record created in PostgreSQL
                             Signed webhook delivered to studio's webhookUrl
5. Match Lifecycle       →  Studio calls /start → IN_PROGRESS
                             Studio calls /complete → result submitted, 202 Accepted
6. Async Processing      →  Kafka consumer updates ELO + Redis leaderboard atomically

Key Engineering Decisions

API Key: Prefix + Secret Split

BCrypt is intentionally slow — verifying the full key on every request without a lookup mechanism would require scanning all rows. The prefix enables O(1) indexed DB lookup, then BCrypt verifies only against that single row. Same model used by Stripe.

Single Kafka Consumer (not two)

Separate RatingConsumer + LeaderboardConsumer on the same topic create a race condition — the leaderboard consumer can read stale ratings from DB before the rating consumer commits. A single MatchCompletedConsumer processes ELO calculation, DB write, and Redis leaderboard update atomically in one transaction — guaranteed correct ordering.

202 Accepted on Match Complete

Rating and leaderboard updates are async via Kafka. Blocking the HTTP response on Kafka consumer speed would couple match completion latency to internal processing. 202 decouples the external contract from internal async work.

PostgreSQL as Source of Truth (not Redis)

Redis is volatile — data can be lost on restart without persistence configured. Ratings, match history, and player data must survive infrastructure failures. Redis is used only for ephemeral data: matchmaking queues (transient by design) and leaderboard (reconstructable from DB).

Flyway for Schema Management

hibernate.ddl-auto=update cannot drop columns, has no rollback, and makes implicit changes. Flyway provides version-controlled SQL migrations — every schema change is explicit, tracked, and reproducible across environments.

Webhooks for External Notification (not Kafka)

PvPEngine is a B2B SaaS platform — game studios have their own infrastructure. Exposing a Kafka cluster externally is a security and operational liability. Webhooks are the industry-standard external integration contract (Stripe, Twilio, GitHub). Kafka stays internal.


ELO Rating System

Standard ELO with K-factor 32. Designed as a pluggable component — the calculation logic is isolated in a single method, replaceable with Glicko-2, TrueSkill, or a custom algorithm without touching any other code.

Parameter Value
Formula New Rating = Old Rating + K × (Actual − Expected)
Expected Score 1 / (1 + 10^((opponentRating − playerRating) / 400))
K Factor 32
Win Actual = 1.0
Loss Actual = 0.0
Draw Actual = 0.5 for both players
Rating Floor 100 (cannot go below)

An upset (low-rated player beating high-rated) yields more rating gain than an expected win. A high-rated player who draws against a much lower-rated opponent loses rating — they underperformed expectations.


Idempotency & Exactly-Once Semantics

Kafka delivers events at-least-once. Duplicate events must not corrupt ratings or leaderboard.

Every consumer checks the processed_events table before processing:

BEGIN transaction
  INSERT INTO processed_events (consumer_group, event_id)  ← unique constraint
  If duplicate key violation → already processed → return safely
  Apply business logic (ELO update + leaderboard update)
COMMIT

This guarantees exactly-once semantics at the application level, even with at-least-once Kafka delivery.


Security Design

  • API keys never stored raw — BCrypt hash only
  • Tenant isolation enforced at filter level — cannot be bypassed at service level
  • gameId never accepted from request body — always resolved from API key
  • All DB queries include gameId — cross-tenant data access is structurally impossible
  • Webhook payloads signed with HMAC-SHA256 — studios can verify authenticity
  • API key expiry and instant revocation supported

API Reference

Public (No Auth Required)

Method Endpoint Description
POST /api/v1/games Onboard a new game studio
POST /api/v1/games/{id}/keys Generate API key — raw key shown once only
GET /api/v1/games/{id} Get game details
DELETE /api/v1/games/{id}/keys/{keyId} Revoke API key

Authenticated (X-API-Key Header Required)

Method Endpoint Description
POST /api/v1/players Create player
GET /api/v1/players/{id} Get player by ID
GET /api/v1/players/username/{username} Get player by username
GET /api/v1/players/external/{externalId} Get player by external ID
PATCH /api/v1/players/{id}/ban Ban player
PATCH /api/v1/players/{id}/unban Unban player
POST /api/v1/matchmaking/join Join matchmaking queue
POST /api/v1/matchmaking/leave Leave matchmaking queue
POST /api/v1/matches/{id}/start Mark match as IN_PROGRESS
POST /api/v1/matches/{id}/complete Submit match result (202 Accepted)
GET /api/v1/leaderboard Get top N players by rating

Webhook Delivery

When a match is found, PvPEngine delivers a signed HTTP POST to the studio's configured webhookUrl.

  • Signature: X-PvP-Signature header — HMAC-SHA256 of the payload body
  • Retry policy: Exponential backoff — 30s → 60s → 120s → 240s → 480s
  • Audit trail: Every delivery attempt logged in webhook_deliveries table (status, request, response, timestamps)

Studios verify the signature to ensure the payload originated from PvPEngine and was not tampered with.


Running Locally

Prerequisites

  • Docker & Docker Compose
  • Java 21
  • Maven

Start Infrastructure

docker-compose up -d

This starts PostgreSQL, Redis, and Kafka.

Run the Application

./mvnw spring-boot:run

Flyway migrations run automatically on startup. Swagger UI available at http://localhost:8080/swagger-ui.html.


Future Roadmap

Competitive Integrity

  • Anti-smurf detection (win rate velocity in first N games)
  • Anti-boosting detection (repeated pairing patterns, IP analysis)
  • Rage quit / abandon tracking with matchmaking penalties
  • Player report system

Matchmaking Quality

  • SBMM rating window expansion — ±25 → ±50 → ±100 → ±200 over wait time
  • Region-based matchmaking — queue:{gameId}:{region}
  • Party matchmaking — group queue join

Platform

  • Seasonal rating resets with rank tiers (Bronze → Diamond)
  • Match history and rating history APIs
  • Game health dashboard — queue sizes, match rates, webhook success rates
  • Rate limiting per tenant (Redis-based)
  • Stripe-based plan management
  • Observability — Micrometer + Prometheus + Grafana
  • Integration tests — Testcontainers (PostgreSQL + Redis + Kafka)

About

PvPEngine is a “Stripe for competitive gaming” — a backend that provides matchmaking, match tracking, ratings, and leaderboards for any game via simple APIs.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages