"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.
| 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 |
- 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
- 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
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
| 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 |
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
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.
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.
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.
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).
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.
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.
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.
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.
- API keys never stored raw — BCrypt hash only
- Tenant isolation enforced at filter level — cannot be bypassed at service level
gameIdnever 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
| 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 |
| 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 |
When a match is found, PvPEngine delivers a signed HTTP POST to the studio's configured webhookUrl.
- Signature:
X-PvP-Signatureheader — HMAC-SHA256 of the payload body - Retry policy: Exponential backoff — 30s → 60s → 120s → 240s → 480s
- Audit trail: Every delivery attempt logged in
webhook_deliveriestable (status, request, response, timestamps)
Studios verify the signature to ensure the payload originated from PvPEngine and was not tampered with.
- Docker & Docker Compose
- Java 21
- Maven
docker-compose up -dThis starts PostgreSQL, Redis, and Kafka.
./mvnw spring-boot:runFlyway migrations run automatically on startup. Swagger UI available at http://localhost:8080/swagger-ui.html.
- 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
- SBMM rating window expansion — ±25 → ±50 → ±100 → ±200 over wait time
- Region-based matchmaking —
queue:{gameId}:{region} - Party matchmaking — group queue join
- 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)